Repository: aws-cloudformation/rain
Pull request #390 (2024-05-18 12:12:07)

Implement a "User Initiated" filter option for the rain logs command
*Issue #, if available:*
#236

*Description of changes:*
Add -s / --since-user-initiated flag for only showing logs since the last 'User Initiated' event

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.



Patch:
diff --git a/cft/cft.go b/cft/cft.go
index dba43c7f..09293cac 100644
--- a/cft/cft.go
+++ b/cft/cft.go
@@ -147,22 +147,3 @@ func (t Template) GetTypes() ([]string, error) {
 
 	return retval, nil
 }
-
-func (t Template) GetResourcesOfType(typeName string) []*yaml.Node {
-	resources, err := t.GetSection(Resources)
-	if err != nil {
-		return nil
-	}
-	retval := make([]*yaml.Node, 0)
-	for i := 0; i < len(resources.Content); i += 2 {
-		resource := resources.Content[i+1]
-		_, typ, _ := s11n.GetMapValue(resource, "Type")
-		if typ == nil {
-			continue
-		}
-		if typ.Value == typeName {
-			retval = append(retval, resource)
-		}
-	}
-	return retval
-}
diff --git a/cft/graph/util.go b/cft/graph/util.go
index f78ac32e..5efdafa9 100644
--- a/cft/graph/util.go
+++ b/cft/graph/util.go
@@ -2,44 +2,18 @@ package graph
 
 import (
 	"fmt"
+	"regexp"
 	"strings"
 
-	"github.com/aws-cloudformation/rain/cft/parse"
 	"github.com/aws-cloudformation/rain/internal/config"
 )
 
+var subRe = regexp.MustCompile(`\$\{([^!].+?)\}`)
+
 func getRefs(t map[string]interface{}) []string {
 	return findRefs(t)
 }
 
-func parseSubString(refs []string, substr string) []string {
-	words, err := parse.ParseSub(substr)
-	if err != nil {
-		config.Debugf("Unable to parse Sub %s: %v", substr, err)
-		return refs
-	}
-
-	for _, word := range words {
-		switch word.T {
-		case parse.AWS:
-			refs = append(refs, fmt.Sprintf("AWS::%s", word.W))
-		case parse.REF:
-			refs = append(refs, word.W)
-		case parse.GETATT:
-			left, _, found := strings.Cut(word.W, ".")
-			if !found {
-				config.Debugf("unexpected GetAtt %s", word.W)
-			} else {
-				refs = append(refs, left)
-			}
-		}
-	}
-
-	config.Debugf("After parsing Sub, refs is now: %v", refs)
-
-	return refs
-}
-
 func findRefs(t map[string]interface{}) []string {
 	refs := make([]string, 0)
 
@@ -73,7 +47,9 @@ func findRefs(t map[string]interface{}) []string {
 		case "Fn::Sub":
 			switch v := value.(type) {
 			case string:
-				refs = parseSubString(refs, v)
+				for _, groups := range subRe.FindAllStringSubmatch(v, 1) {
+					refs = append(refs, groups[1])
+				}
 			case []interface{}:
 				switch {
 				case len(v) != 2:
diff --git a/cft/pkg/directives.go b/cft/pkg/directives.go
index 2962c2a7..bac2777a 100644
--- a/cft/pkg/directives.go
+++ b/cft/pkg/directives.go
@@ -1,6 +1,5 @@
-package pkg
-
 // This file contains implementations for `!Rain::` directives
+package pkg
 
 import (
 	"embed"
@@ -40,9 +39,6 @@ type directiveContext struct {
 	t       cft.Template
 	parent  node.NodePair
 	fs      *embed.FS
-
-	// baseUri is the base path for downloading submodules
-	baseUri string
 }
 
 // A !Rain directive implementation
@@ -66,10 +62,7 @@ func includeString(ctx *directiveContext) (bool, error) {
 		return false, err
 	}
 
-	err = ctx.n.Encode(strings.TrimSpace(string(content)))
-	if err != nil {
-		return false, err
-	}
+	ctx.n.Encode(strings.TrimSpace(string(content)))
 
 	return true, nil
 }
@@ -87,11 +80,7 @@ func includeLiteral(ctx *directiveContext) (bool, error) {
 	}
 
 	// Transform
-	err = parse.NormalizeNode(&contentNode)
-	if err != nil {
-		return false, err
-	}
-
+	parse.NormalizeNode(&contentNode)
 	_, err = transform(&transformContext{
 		nodeToTransform: &contentNode,
 		rootDir:         filepath.Dir(path),
@@ -119,10 +108,7 @@ func includeEnv(ctx *directiveContext) (bool, error) {
 		return false, fmt.Errorf("missing environmental variable %q", name)
 	}
 	var newNode yaml.Node
-	err = newNode.Encode(val)
-	if err != nil {
-		return false, err
-	}
+	newNode.Encode(val)
 	if err != nil {
 		return false, err
 	}
@@ -157,20 +143,11 @@ func handleS3(root string, options s3Options) (*yaml.Node, error) {
 			options.KeyProperty:    s.key,
 		}
 
-		err := n.Encode(out)
-		if err != nil {
-			return nil, err
-		}
+		n.Encode(out)
 	case s3URI:
-		err := n.Encode(s.URI())
-		if err != nil {
-			return nil, err
-		}
+		n.Encode(s.URI())
 	case s3Http:
-		err := n.Encode(s.HTTP())
-		if err != nil {
-			return nil, err
-		}
+		n.Encode(s.HTTP())
 	default:
 		return nil, fmt.Errorf("unexpected S3 output format: %s", options.Format)
 	}
@@ -277,9 +254,9 @@ func includeS3(ctx *directiveContext) (bool, error) {
 	}
 
 	if n.Content[1].Kind == yaml.ScalarNode {
-		return includeS3URI(&directiveContext{n, root, t, parent, nil, ""})
+		return includeS3URI(&directiveContext{n, root, t, parent, nil})
 	} else if n.Content[1].Kind == yaml.MappingNode {
-		return includeS3Object(&directiveContext{n, root, t, parent, nil, ""})
+		return includeS3Object(&directiveContext{n, root, t, parent, nil})
 	}
 
 	return true, nil
diff --git a/cft/pkg/module.go b/cft/pkg/module.go
index a55f0b0d..95326cec 100644
--- a/cft/pkg/module.go
+++ b/cft/pkg/module.go
@@ -554,7 +554,7 @@ func processModule(
 				continue
 			}
 			if plProps != nil {
-				// Get rid of what we cloned, so we can replace it entirely
+				// Get rid of what we cloned so we can replace it entirely
 				node.RemoveFromMap(clonedResource, pl)
 			}
 			clonedResource.Content = append(clonedResource.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: pl})
@@ -577,7 +577,7 @@ func processModule(
 			clonedResource.Content = append(clonedResource.Content, &yaml.Node{Kind: yaml.ScalarNode, Value: "DependsOn"})
 			dependsOnValue := &yaml.Node{Kind: yaml.SequenceNode, Content: make([]*yaml.Node, 0)}
 			if moduleDependsOn != nil {
-				// Remove the original DependsOn, so we don't end up with two
+				// Remove the original DependsOn so we don't end up with two
 				node.RemoveFromMap(clonedResource, "DependsOn")
 
 				// Change the names to the modified resource name for the template
@@ -709,33 +709,9 @@ func processModule(
 	return true, nil
 }
 
-// downloadModule downloads the file from the given URI and returns its content as a byte slice.
-func downloadModule(uri string) ([]byte, error) {
-	config.Debugf("Downloading %s", uri)
-	resp, err := http.Get(uri)
-	if err != nil {
-		return nil, err
-	}
-	defer func(Body io.ReadCloser) {
-		err := Body.Close()
-		if err != nil {
-			config.Debugf("Error closing body: %v", err)
-		}
-	}(resp.Body)
-
-	content, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return nil, err
-	}
-
-	return content, nil
-}
-
 // Type: !Rain::Module
 func module(ctx *directiveContext) (bool, error) {
 
-	config.Debugf("module directiveContext: %+v", ctx)
-
 	n := ctx.n
 	root := ctx.rootDir
 	t := ctx.t
@@ -756,32 +732,21 @@ func module(ctx *directiveContext) (bool, error) {
 	var path string
 	var newRootDir string
 
-	baseUri := ctx.baseUri
-
 	// Is this a local file or a URL?
 	if strings.HasPrefix(uri, "https://") {
-
-		content, err = downloadModule(uri)
+		// Download the file and then parse it
+		resp, err := http.Get(uri)
 		if err != nil {
 			return false, err
 		}
-
-		// Once we see a URL instead of a relative local path,
-		// we need to remember the base URL so that we can
-		// fix relative paths in any referenced modules.
-
-		// Strip the file name from the uri
-		urlParts := strings.Split(uri, "/")
-		baseUri = strings.Join(urlParts[:len(urlParts)-1], "/")
+		defer resp.Body.Close()
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return false, err
+		}
+		content = []byte(body)
 	} else {
-		if baseUri != "" {
-			// If we have a base URL, prepend it to the relative path
-			uri = baseUri + "/" + uri
-			content, err = downloadModule(uri)
-			if err != nil {
-				return false, err
-			}
-		} else if templateFiles != nil {
+		if templateFiles != nil {
 			// Read from the embedded file system (for the build -r command)
 			path, err = expectString(n)
 			if err != nil {
@@ -813,11 +778,8 @@ func module(ctx *directiveContext) (bool, error) {
 		return false, err
 	}
 
-	err = parse.NormalizeNode(&moduleNode)
-	if err != nil {
-		return false, err
-	}
-
+	// Recurse on directives in the module itself
+	parse.NormalizeNode(&moduleNode)
 	var newParent node.NodePair
 	if parent.Parent != nil && parent.Parent.Value != nil {
 		newParent = node.GetParent(n, parent.Parent.Value, nil)
@@ -830,7 +792,6 @@ func module(ctx *directiveContext) (bool, error) {
 		t:               cft.Template{Node: &moduleNode},
 		parent:          &newParent,
 		fs:              ctx.fs,
-		baseUri:         baseUri,
 	})
 	if err != nil {
 		return false, err
diff --git a/cft/pkg/pkg.go b/cft/pkg/pkg.go
index 62a33076..c26bdda8 100644
--- a/cft/pkg/pkg.go
+++ b/cft/pkg/pkg.go
@@ -2,7 +2,7 @@
 // but has greater flexibility, allowing content to be included anywhere in a template
 //
 // To include content into your templates, use any of the following either as YAML tags
-// or as one-property objects, much as AWS intrinsic functions are used, e.g. "Fn::Join"
+// or as one-property objects, much as AWS instrinsic functions are used, e.g. "Fn::Join"
 //
 // `Rain::Include`: insert the content of the file into the template directly. The file must be in YAML or JSON format.
 // `Rain::Env`: inserts environmental variable value into the template as a string. Variable must be set.
@@ -42,7 +42,7 @@ import (
 	"gopkg.in/yaml.v3"
 )
 
-// Experimental must be set to true to enable !Rain::Module
+// Must be set to true to enable !Rain::Module
 var Experimental bool
 
 type transformContext struct {
@@ -50,10 +50,7 @@ type transformContext struct {
 	rootDir         string // Using normal files
 	t               cft.Template
 	parent          *node.NodePair
-	fs              *embed.FS // Used by build with embedded filesystem
-
-	// baseUri is the base path for downloading submodules
-	baseUri string
+	fs              *embed.FS // Used by build with embedded filesytem
 }
 
 func transform(ctx *transformContext) (bool, error) {
@@ -64,7 +61,7 @@ func transform(ctx *transformContext) (bool, error) {
 		for found := range s11n.MatchAll(ctx.nodeToTransform, path) {
 			nodeParent := node.GetParent(found, ctx.nodeToTransform, nil)
 			nodeParent.Parent = ctx.parent
-			c, err := fn(&directiveContext{found, ctx.rootDir, ctx.t, nodeParent, ctx.fs, ctx.baseUri})
+			c, err := fn(&directiveContext{found, ctx.rootDir, ctx.t, nodeParent, ctx.fs})
 			if err != nil {
 				config.Debugf("Error packaging template: %s\n", err)
 				return false, err
@@ -93,14 +90,14 @@ func Template(t cft.Template, rootDir string, fs *embed.FS) (cft.Template, error
 		parent:          &node.NodePair{Key: t.Node, Value: t.Node},
 		fs:              fs,
 	}
-	var changed = false
+	var changed bool = false
 	passes := 0
 	maxPasses := 100
 	for {
 		passes += 1
 		// Modules can add new nodes to the template, which
 		// breaks s11n.MatchAll, since it expects the length to stay the same.
-		// Just start over and transform the whole template again.
+		// Just start over a transform the whole template again.
 		changedThisPass, err := transform(ctx)
 		if err != nil {
 			return t, err
@@ -185,11 +182,11 @@ func File(path string) (cft.Template, error) {
 	var err error
 
 	if strings.HasSuffix(path, ".pkl") {
-		y, err := rainpkl.Yaml(path)
+		yaml, err := rainpkl.Yaml(path)
 		if err != nil {
 			return t, err
 		}
-		t, err = parse.String(y)
+		t, err = parse.String(yaml)
 		if err != nil {
 			return t, err
 		}
diff --git a/go.mod b/go.mod
index 734c8854..b333cfcc 100644
--- a/go.mod
+++ b/go.mod
@@ -7,14 +7,14 @@ toolchain go1.21.1
 require (
 	//github.com/ake-persson/mapslice-json v0.0.0-20210720081907-22c8edf57807
 	github.com/appscode/jsonpatch v1.0.1
-	github.com/aws/aws-sdk-go-v2 v1.27.0
-	github.com/aws/aws-sdk-go-v2/config v1.27.16
-	github.com/aws/aws-sdk-go-v2/credentials v1.17.16
-	github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.3
+	github.com/aws/aws-sdk-go-v2 v1.26.1
+	github.com/aws/aws-sdk-go-v2/config v1.27.11
+	github.com/aws/aws-sdk-go-v2/credentials v1.17.11
+	github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.0
 	github.com/aws/aws-sdk-go-v2/service/cloudformation v1.50.1
-	github.com/aws/aws-sdk-go-v2/service/ec2 v1.161.3
-	github.com/aws/aws-sdk-go-v2/service/s3 v1.54.3
-	github.com/aws/aws-sdk-go-v2/service/sts v1.28.10
+	github.com/aws/aws-sdk-go-v2/service/ec2 v1.160.0
+	github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1
+	github.com/aws/aws-sdk-go-v2/service/sts v1.28.7
 	github.com/aws/smithy-go v1.20.2
 	github.com/chzyer/readline v1.5.1
 	github.com/google/go-cmp v0.6.0
@@ -30,12 +30,9 @@ require (
 
 require (
 	github.com/apple/pkl-go v0.6.1-0.20240320183646-5827572b8112
-	github.com/aws/aws-sdk-go-v2/service/acm v1.25.5
-	github.com/aws/aws-sdk-go-v2/service/kms v1.32.1
-	github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.3
+	github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.0
 	github.com/aws/aws-sdk-go-v2/service/rds v1.78.1
 	github.com/aws/aws-sdk-go-v2/service/servicequotas v1.21.4
-	github.com/aws/aws-sdk-go-v2/service/ssm v1.50.1
 	github.com/fatih/color v1.16.0
 	github.com/manifoldco/promptui v0.9.0
 	github.com/mattn/go-runewidth v0.0.15
@@ -43,8 +40,8 @@ require (
 )
 
 require (
-	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.7 // indirect
-	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 // indirect
+	github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
 	github.com/mattn/go-isatty v0.0.20 // indirect
@@ -56,17 +53,17 @@ require (
 
 require (
 	github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 // indirect
-	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 // indirect
-	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 // indirect
+	github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect
+	github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect
 	github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
 	github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.18.5
-	github.com/aws/aws-sdk-go-v2/service/iam v1.32.4
+	github.com/aws/aws-sdk-go-v2/service/iam v1.32.1
 	github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.9 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 // indirect
-	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.7 // indirect
-	github.com/aws/aws-sdk-go-v2/service/sso v1.20.9 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 // indirect
+	github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 // indirect
+	github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 // indirect
 	github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
diff --git a/go.sum b/go.sum
index 90c76e8c..0115b5e6 100644
--- a/go.sum
+++ b/go.sum
@@ -2,62 +2,56 @@ github.com/apple/pkl-go v0.6.1-0.20240320183646-5827572b8112 h1:2jH92uAgLXV4sQ1Z
 github.com/apple/pkl-go v0.6.1-0.20240320183646-5827572b8112/go.mod h1:xr5s9RAJdlEHU2efRenGiWkE0gssttQs0LE1HyBY2LQ=
 github.com/appscode/jsonpatch v1.0.1 h1:e82Bj+rsBSnpsmjiIGlc9NiKSBpJONZkamk/F8GrCR0=
 github.com/appscode/jsonpatch v1.0.1/go.mod h1:4AJxUpXUhv4N+ziTvIcWWXgeorXpxPZOfk9HdEVr96M=
-github.com/aws/aws-sdk-go-v2 v1.27.0 h1:7bZWKoXhzI+mMR/HjdMx8ZCC5+6fY0lS5tr0bbgiLlo=
-github.com/aws/aws-sdk-go-v2 v1.27.0/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
+github.com/aws/aws-sdk-go-v2 v1.26.1 h1:5554eUqIYVWpU0YmeeYZ0wU64H2VLBs8TlhRB2L+EkA=
+github.com/aws/aws-sdk-go-v2 v1.26.1/go.mod h1:ffIFB97e2yNsv4aTSGkqtHnppsIJzw7G7BReUZ3jCXM=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2 h1:x6xsQXGSmW6frevwDA+vi/wqhp1ct18mVXYN08/93to=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.2/go.mod h1:lPprDr1e6cJdyYeGXnRaJoP4Md+cDBvi2eOj00BlGmg=
-github.com/aws/aws-sdk-go-v2/config v1.27.16 h1:knpCuH7laFVGYTNd99Ns5t+8PuRjDn4HnnZK48csipM=
-github.com/aws/aws-sdk-go-v2/config v1.27.16/go.mod h1:vutqgRhDUktwSge3hrC3nkuirzkJ4E/mLj5GvI0BQas=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.16 h1:7d2QxY83uYl0l58ceyiSpxg9bSbStqBC6BeEeHEchwo=
-github.com/aws/aws-sdk-go-v2/credentials v1.17.16/go.mod h1:Ae6li/6Yc6eMzysRL2BXlPYvnrLLBg3D11/AmOjw50k=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3 h1:dQLK4TjtnlRGb0czOht2CevZ5l6RSyRWAnKeGd7VAFE=
-github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.3/go.mod h1:TL79f2P6+8Q7dTsILpiVST+AL9lkF6PPGI167Ny0Cjw=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7 h1:lf/8VTF2cM+N4SLzaYJERKEWAXq8MOMpZfU6wEPWsPk=
-github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.7/go.mod h1:4SjkU7QiqK2M9oozyMzfZ/23LmUY+h3oFqhdeP5OMiI=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7 h1:4OYVp0705xu8yjdyoWix0r9wPIRXnIzzOoUpQVHIJ/g=
-github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.7/go.mod h1:vd7ESTEvI76T2Na050gODNmNU7+OyKrIKroYTu4ABiI=
+github.com/aws/aws-sdk-go-v2/config v1.27.11 h1:f47rANd2LQEYHda2ddSCKYId18/8BhSRM4BULGmfgNA=
+github.com/aws/aws-sdk-go-v2/config v1.27.11/go.mod h1:SMsV78RIOYdve1vf36z8LmnszlRWkwMQtomCAI0/mIE=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.11 h1:YuIB1dJNf1Re822rriUOTxopaHHvIq0l/pX3fwO+Tzs=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.11/go.mod h1:AQtFPsDH9bI2O+71anW6EKL+NcD7LG3dpKGMV4SShgo=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 h1:FVJ0r5XTHSmIHJV6KuDmdYhEpvlHpiSd38RQWhut5J4=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1/go.mod h1:zusuAeqezXzAB24LGuzuekqMAEgWkVYukBec3kr3jUg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 h1:aw39xVGeRWlWx9EzGVnhOR4yOjQDHPQ6o6NmBlscyQg=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5/go.mod h1:FSaRudD0dXiMPK2UjknVwwTYyZMRsHv3TtkabsZih5I=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 h1:PG1F3OD1szkuQPzDw3CIQsRIrtTlUC3lP84taWzHlq0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5/go.mod h1:jU1li6RFryMz+so64PpKtudI+QzbKoIEivqdf6LNpOc=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
 github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.7 h1:/FUtT3xsoHO3cfh+I/kCbcMCN98QZRsiFet/V8QkWSs=
-github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.7/go.mod h1:MaCAgWpGooQoCWZnMur97rGn5dp350w2+CeiV5406wE=
-github.com/aws/aws-sdk-go-v2/service/acm v1.25.5 h1:VUFUI8yF8Jgv6DtjS3eBcIsWrZzOsQ9qNzqEh8EhYEY=
-github.com/aws/aws-sdk-go-v2/service/acm v1.25.5/go.mod h1:kTFYiaoqqRsZC+BYdciI5tFLtuodontKG5jGjCGtPUg=
-github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.3 h1:Fihjyd6DeNjcawBEGLH9dkIEUi6AdhucDKPE9nJ4QiY=
-github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.3/go.mod h1:opvUj3ismqSCxYc+m4WIjPL0ewZGtvp0ess7cKvBPOQ=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5 h1:81KE7vaZzrl7yHBYHVEzYB8sypz11NMOZ40YlWvPxsU=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.5/go.mod h1:LIt2rg7Mcgn09Ygbdh/RdIm0rQ+3BNkbP1gyVMFtRK0=
+github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.0 h1:kt4JDYAIjygWfuBPMtmjgp2Dnd1HckQGJ5pnS6Q7eLY=
+github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.8.0/go.mod h1:nZspkhg+9p8iApLFoyAqfyuMP0F38acy2Hm3r5r95Cg=
 github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.18.5 h1:8s0iOBAAUEq0m51PWmaCuEfrdH0OQkXyGL4+jgQoIJ8=
 github.com/aws/aws-sdk-go-v2/service/cloudcontrol v1.18.5/go.mod h1:oOvzqGwjzl5fyWi0C7YfOalzMDS8R4yapREwUVV5gBY=
 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.50.1 h1:lFyHV8TCw4GRj39hvnGzENfTEFPPUpRwC8cqaAPp8dk=
 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.50.1/go.mod h1:/v2KYdCW4BaHKayenaWEXOOdxItIwEA3oU0XzuQY3F0=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.161.3 h1:l0mvKOGm25yo/Fy+Y/08Cm4aTA4XmnIuq4ppy+shfMI=
-github.com/aws/aws-sdk-go-v2/service/ec2 v1.161.3/go.mod h1:iJ2sQeUTkjNp3nL7kE/Bav0xXYhtiRCRP5ZXk4jFhCQ=
-github.com/aws/aws-sdk-go-v2/service/iam v1.32.4 h1:SPnvgZQ0TXvzs/On+BBUYHVyadSV3WQDvsk+G99wjYA=
-github.com/aws/aws-sdk-go-v2/service/iam v1.32.4/go.mod h1:0xqsq1/HsAC7+OaRMFUHfFtM5wmuFeX4VlbpxNAc2qY=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.160.0 h1:ooy0OFbrdSwgk32OFGPnvBwry5ySYCKkgTEbQ2hejs8=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.160.0/go.mod h1:xejKuuRDjz6z5OqyeLsz01MlOqqW7CqpAB4PabNvpu8=
+github.com/aws/aws-sdk-go-v2/service/iam v1.32.1 h1:4rE8nIQ7HabhytHpGacgyLF4NjsswF4rBe7smA2kxa0=
+github.com/aws/aws-sdk-go-v2/service/iam v1.32.1/go.mod h1:aXWImQV0uTW35LM0A/T4wEg6R1/ReXUu4SM6/lUHYK0=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2 h1:Ji0DY1xUsUr3I8cHps0G+XM3WWU16lP6yG8qu1GAZAs=
 github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.2/go.mod h1:5CsjAbs3NlGQyZNFACh+zztPDI7fU6eW9QsxjfnuBKg=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.9 h1:UXqEWQI0n+q0QixzU0yUUQBZXRd5037qdInTIHFTl98=
-github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.9/go.mod h1:xP6Gq6fzGZT8w/ZN+XvGMZ2RU1LeEs7b2yUP5DN8NY4=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9 h1:Wx0rlZoEJR7JwlSZcHnEa7CNjrSIyVxMFWGAaXy4fJY=
-github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.9/go.mod h1:aVMHdE0aHO3v+f/iw01fmXV/5DbfQ3Bi9nN7nd9bE9Y=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.7 h1:uO5XR6QGBcmPyo2gxofYJLFkcVQ4izOoGDNenlZhTEk=
-github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.7/go.mod h1:feeeAYfAcwTReM6vbwjEyDmiGho+YgBhaFULuXDW8kc=
-github.com/aws/aws-sdk-go-v2/service/kms v1.32.1 h1:FARrQLRQXpCFYylIUVF1dRij6YbPCmtwudq9NBk4kFc=
-github.com/aws/aws-sdk-go-v2/service/kms v1.32.1/go.mod h1:8lETO9lelSG2B6KMXFh2OwPPqGV6WQM3RqLAEjP1xaU=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.3 h1:ClOI0GZ62/I3MfSK9GO7dnqCLxz0QiMQOPRX+f+zvEs=
-github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.3/go.mod h1:KHTaLdvivCfFDAE8jed5OogP1l+GYhrsaLTOV3honIQ=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7 h1:ZMeFZ5yk+Ek+jNr1+uwCd2tG89t6oTS5yVWpa6yy2es=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.7/go.mod h1:mxV05U+4JiHqIpGqqYXOHLPKUC6bDXC44bsUhNjOEwY=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7 h1:ogRAwT1/gxJBcSWDMZlgyFUM962F51A5CRhDLbxLdmo=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.7/go.mod h1:YCsIZhXfRPLFFCl5xxY+1T9RKzOKjCut+28JSX2DnAk=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5 h1:f9RyWNtS8oH7cZlbn+/JNPpjUk5+5fLd5lM9M0i49Ys=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.5/go.mod h1:h5CoMZV2VF297/VLhRhO1WF+XYWOzXo+4HsObA4HjBQ=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.0 h1:lfVUMJEGXzi5l8jam/WXLNSn+vM/fpe2dmMYOdRiQ+k=
+github.com/aws/aws-sdk-go-v2/service/lightsail v1.37.0/go.mod h1:GSVUed6FJivX6v7Pgrk9iXuRa2NuCtT+nMWGxQHSAXQ=
 github.com/aws/aws-sdk-go-v2/service/rds v1.78.1 h1:D3XX2O6IzStNWEK2GU5EQTZVZ2r2Q4aRhWplndGIQR4=
 github.com/aws/aws-sdk-go-v2/service/rds v1.78.1/go.mod h1:Rw15qGaGWu3jO0dOz7JyvdOEjgae//YrJxVWLYGynvg=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.54.3 h1:57NtjG+WLims0TxIQbjTqebZUKDM03DfM11ANAekW0s=
-github.com/aws/aws-sdk-go-v2/service/s3 v1.54.3/go.mod h1:739CllldowZiPPsDFcJHNF4FXrVxaSGVnZ9Ez9Iz9hc=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1 h1:6cnno47Me9bRykw9AEv9zkXE+5or7jz8TsskTTccbgc=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.53.1/go.mod h1:qmdkIIAC+GCLASF7R2whgNrJADz0QZPX+Seiw/i4S3o=
 github.com/aws/aws-sdk-go-v2/service/servicequotas v1.21.4 h1:SSDkZRAO8Ok5SoQ4BJ0onDeb0ga8JBOCkUmNEpRChcw=
 github.com/aws/aws-sdk-go-v2/service/servicequotas v1.21.4/go.mod h1:plXue/Zg49kU3uU6WwfCWgRR5SRINNiJf03Y/UhYOhU=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.50.1 h1:vgpeoBRWw22qcb1xo3eJFkuulwPI4E/xQgIGi0gtVUs=
-github.com/aws/aws-sdk-go-v2/service/ssm v1.50.1/go.mod h1:Ebk/HZmGhxWKDVxM4+pwbxGjm3RQOQLMjAEosI3ss9Q=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.9 h1:aD7AGQhvPuAxlSUfo0CWU7s6FpkbyykMhGYMvlqTjVs=
-github.com/aws/aws-sdk-go-v2/service/sso v1.20.9/go.mod h1:c1qtZUWtygI6ZdvKppzCSXsDOq5I4luJPZ0Ud3juFCA=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3 h1:Pav5q3cA260Zqez42T9UhIlsd9QeypszRPwC9LdSSsQ=
-github.com/aws/aws-sdk-go-v2/service/ssooidc v1.24.3/go.mod h1:9lmoVDVLz/yUZwLaQ676TK02fhCu4+PgRSmMaKR1ozk=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.10 h1:69tpbPED7jKPyzMcrwSvhWcJ9bPnZsZs18NT40JwM0g=
-github.com/aws/aws-sdk-go-v2/service/sts v1.28.10/go.mod h1:0Aqn1MnEuitqfsCNyKsdKLhDUOr4txD/g19EfiUqgws=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.5 h1:vN8hEbpRnL7+Hopy9dzmRle1xmDc7o8tmY0klsr175w=
+github.com/aws/aws-sdk-go-v2/service/sso v1.20.5/go.mod h1:qGzynb/msuZIE8I75DVRCUXw3o3ZyBmUvMwQ2t/BrGM=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4 h1:Jux+gDDyi1Lruk+KHF91tK2KCuY61kzoCpvtvJJBtOE=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.23.4/go.mod h1:mUYPBhaF2lGiukDEjJX2BLRRKTmoUSitGDUgM4tRxak=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.7 h1:et3Ta53gotFR4ERLXXHIHl/Uuk1qYpP5uU7cvNql8ns=
+github.com/aws/aws-sdk-go-v2/service/sts v1.28.7/go.mod h1:FZf1/nKNEkHdGGJP/cI2MoIMquumuRK6ol3QQJNDxmw=
 github.com/aws/smithy-go v1.20.2 h1:tbp628ireGtzcHDDmLT/6ADHidqnwgF57XOXZe6tp4Q=
 github.com/aws/smithy-go v1.20.2/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
 github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
diff --git a/internal/aws/acm/acm.go b/internal/aws/acm/acm.go
deleted file mode 100644
index 06676733..00000000
--- a/internal/aws/acm/acm.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package acm
-
-import (
-	"context"
-	rainaws "github.com/aws-cloudformation/rain/internal/aws"
-	"github.com/aws-cloudformation/rain/internal/config"
-	"github.com/aws/aws-sdk-go-v2/service/acm"
-	"time"
-)
-
-func getClient() *acm.Client {
-	return acm.NewFromConfig(rainaws.Config())
-}
-
-// CheckCertificate checks if the certificate exists and is valid
-func CheckCertificate(arn string) (bool, error) {
-	client := getClient()
-	_, err := client.GetCertificate(context.Background(), &acm.GetCertificateInput{
-		CertificateArn: &arn,
-	})
-
-	if err != nil {
-		return false, err
-	}
-
-	res, err := client.DescribeCertificate(context.Background(), &acm.DescribeCertificateInput{
-		CertificateArn: &arn,
-	})
-	if err != nil {
-		return false, err
-	}
-	// Make sure the cert has not expired
-	if res.Certificate.NotAfter.Before(time.Now()) {
-		config.Debugf("Cert expired: %s", arn)
-		return false, nil
-	}
-
-	return true, nil
-}
diff --git a/internal/aws/ec2/ec2.go b/internal/aws/ec2/ec2.go
index 4bab00ae..e1183afe 100644
--- a/internal/aws/ec2/ec2.go
+++ b/internal/aws/ec2/ec2.go
@@ -4,18 +4,14 @@ package ec2
 
 import (
 	"context"
-	"fmt"
 	"sort"
 
-	rainaws "github.com/aws-cloudformation/rain/internal/aws"
-	"github.com/aws/aws-sdk-go-v2/aws"
-	"github.com/aws/aws-sdk-go-v2/service/ec2/types"
-
+	"github.com/aws-cloudformation/rain/internal/aws"
 	"github.com/aws/aws-sdk-go-v2/service/ec2"
 )
 
 func getClient() *ec2.Client {
-	return ec2.NewFromConfig(rainaws.Config())
+	return ec2.NewFromConfig(aws.Config())
 }
 
 // GetRegions returns all region names as strings
@@ -46,79 +42,3 @@ func CheckKeyPairExists(name string) (bool, error) {
 
 	return len(res.KeyPairs) > 0, nil
 }
-
-func GetInstanceType(instanceType string) (*types.InstanceTypeInfo, error) {
-	res, err := getClient().DescribeInstanceTypes(context.Background(),
-		&ec2.DescribeInstanceTypesInput{
-			InstanceTypes: []types.InstanceType{types.InstanceType(instanceType)},
-		})
-
-	if err != nil {
-		return nil, err
-	}
-
-	if len(res.InstanceTypes) == 0 {
-		return nil, fmt.Errorf("no instance type found for %s", instanceType)
-	}
-
-	return &res.InstanceTypes[0], nil
-}
-
-func GetImage(imageID string) (*types.Image, error) {
-	res, err := getClient().DescribeImages(context.Background(),
-		&ec2.DescribeImagesInput{
-			ImageIds: []string{imageID},
-		})
-
-	if err != nil {
-		return nil, err
-	}
-
-	if len(res.Images) == 0 {
-		return nil, fmt.Errorf("no image found for %s", imageID)
-	}
-
-	return &res.Images[0], nil
-}
-
-func GetInstanceTypesForArchitecture(architecture string) ([]string, error) {
-	res, err := getClient().DescribeInstanceTypes(context.Background(),
-		&ec2.DescribeInstanceTypesInput{
-			Filters: []types.Filter{
-				{
-					Name:   aws.String("processor-info.supported-architecture"),
-					Values: []string{architecture},
-				},
-			},
-		})
-
-	if err != nil {
-		return nil, err
-	}
-
-	retval := make([]string, len(res.InstanceTypes))
-	for i, instanceType := range res.InstanceTypes {
-		retval[i] = string(instanceType.InstanceType)
-	}
-	return retval, nil
-}
-
-// GetDefaultVPCId returns the default VPC Id, or a blank string
-func GetDefaultVPCId() (string, error) {
-
-	output, err := getClient().DescribeVpcs(context.Background(), &ec2.DescribeVpcsInput{})
-	if err != nil {
-		return "", err
-	}
-
-	// Find the default VPC
-	var defaultVpcID string
-	for _, vpc := range output.Vpcs {
-		if *vpc.IsDefault {
-			defaultVpcID = *vpc.VpcId
-			break
-		}
-	}
-
-	return defaultVpcID, nil
-}
diff --git a/internal/aws/ec2/mock.go b/internal/aws/ec2/mock.go
index 87b295cf..b4be35a9 100644
--- a/internal/aws/ec2/mock.go
+++ b/internal/aws/ec2/mock.go
@@ -2,8 +2,6 @@
 
 package ec2
 
-import "github.com/aws/aws-sdk-go-v2/service/ec2/types"
-
 // GetRegions returns all region names as strings
 func GetRegions() ([]string, error) {
 	return []string{
@@ -16,19 +14,3 @@ func GetRegions() ([]string, error) {
 func CheckKeyPairExists(name string) (bool, error) {
 	return true, nil
 }
-
-func GetImage(imageID string) (*types.Image, error) {
-	return nil, nil
-}
-
-func GetInstanceType(instanceType string) (*types.InstanceTypeInfo, error) {
-	return nil, nil
-}
-
-func GetInstanceTypesForArchitecture(architecture string) ([]string, error) {
-	return nil, nil
-}
-
-func GetDefaultVPCId() (string, error) {
-	return "", nil
-}
diff --git a/internal/aws/iam/iam.go b/internal/aws/iam/iam.go
index 401bff0b..e7da4ed6 100644
--- a/internal/aws/iam/iam.go
+++ b/internal/aws/iam/iam.go
@@ -2,13 +2,11 @@ package iam
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
-	"net/url"
 	"regexp"
 	"strings"
 
-	"github.com/aws-cloudformation/rain/internal/aws"
+	aws "github.com/aws-cloudformation/rain/internal/aws"
 	"github.com/aws-cloudformation/rain/internal/config"
 	"github.com/aws-cloudformation/rain/internal/console/spinner"
 	"github.com/aws-cloudformation/rain/internal/s11n"
@@ -23,7 +21,7 @@ func getClient() *iam.Client {
 	return iam.NewFromConfig(aws.Config())
 }
 
-// GetCallerArn gets the role arn of the caller based on the aws config
+// Get the role arn of the caller based on the aws config
 func GetCallerArn(config awsgo.Config) (string, error) {
 	stsClient := sts.NewFromConfig(config)
 	stsRes, stsErr := stsClient.GetCallerIdentity(context.Background(),
@@ -50,8 +48,8 @@ func TransformCallerArn(stsResArn string) string {
 // Will this work consistently for other SSO providers?
 // Is there a programmatic way to retrieve the actual role?
 func convertAssumeRoleToRole(stsResArn string) string {
-	stsStr := strings.Split(stsResArn, "sts::")[1]
-	accountId := strings.Split(stsStr, ":")[0]
+	sts := strings.Split(stsResArn, "sts::")[1]
+	accountId := strings.Split(sts, ":")[0]
 	assumedRole := strings.Split(stsResArn, "assumed-role/")[1]
 	actualRoleName := strings.Split(assumedRole, "/")[0]
 	return fmt.Sprintf("arn:aws:iam::%v:role/%v", accountId, actualRoleName)
@@ -131,23 +129,13 @@ func Simulate(
 	return allowed, messages
 }
 
-func GetRoleNameFromArn(roleArn string) (string, error) {
-	tokens := strings.Split(roleArn, ":role/")
-	if len(tokens) != 2 {
-		return "", fmt.Errorf("invalid role arn: %v", roleArn)
-	}
-	return tokens[1], nil
-}
-
-// RoleExists checks to see if a role exists in the account
+// Check to see if a role exists in the account
 func RoleExists(roleArn string) bool {
-	roleName, err := GetRoleNameFromArn(roleArn)
-	if err != nil {
-		config.Debugf("RoleExists GetRoleNameFromArn Error for %v: %v", roleArn, err)
-		return false
-	}
+	tokens := strings.Split(roleArn, ":role/")
+	lastToken := tokens[len(tokens)-1]
+	config.Debugf("RoleExists %v %v", roleArn, lastToken)
 	res, err := getClient().GetRole(context.Background(), &iam.GetRoleInput{
-		RoleName: &roleName,
+		RoleName: &lastToken,
 	})
 	if err != nil {
 		config.Debugf("RoleExists GetRole Error for %v: %v", roleArn, err)
@@ -157,7 +145,7 @@ func RoleExists(roleArn string) bool {
 	return true
 }
 
-// PrincipalExists checks to see if the principal exists in the account
+// Check to see if the principal exists in the account
 func PrincipalExists(principal string) (bool, error) {
 
 	config.Debugf("PrincipalExists %v", principal)
@@ -201,7 +189,7 @@ func PrincipalExists(principal string) (bool, error) {
 	return false, nil
 }
 
-// CheckPolicyDocument checks a PolicyDocument to make sure it will not result in failures
+// Check a PolicyDocument to make sure it will not result in failures
 func CheckPolicyDocument(doc *yaml.Node) (bool, error) {
 
 	policyOk := true
@@ -217,7 +205,9 @@ func CheckPolicyDocument(doc *yaml.Node) (bool, error) {
 							config.Debugf("About to check if principal exists: %v", p)
 							if p.Kind == yaml.MappingNode {
 								config.Debugf("principal is a map")
-								// TODO - resolve intrinsics if we can
+
+								// This is.. hard. We need to resolve !Sub, !Ref, etc
+								// TODO
 							} else {
 								exists, err := PrincipalExists(p.Value)
 								if err != nil || !exists {
@@ -234,51 +224,3 @@ func CheckPolicyDocument(doc *yaml.Node) (bool, error) {
 
 	return policyOk, nil
 }
-
-// CanAssumeRole checks if a service can assume a role
-func CanAssumeRole(roleArn string, serviceName string) (bool, error) {
-	roleName, err := GetRoleNameFromArn(roleArn)
-	if err != nil {
-		config.Debugf("CanAssumeRole GetRoleNameFromArn Error for %v: %v", roleArn, err)
-		return false, err
-	}
-
-	config.Debugf("CanAssumeRole checking role %v for service %v", roleName, serviceName)
-
-	// Get the role details
-	roleOutput, err := getClient().GetRole(context.Background(),
-		&iam.GetRoleInput{
-			RoleName: awsgo.String(roleName),
-		})
-
-	if err != nil {
-		config.Debugf("CanAssumeRole Error: %v", err)
-		return false, err
-	}
-
-	// Parse the AssumeRolePolicyDocument
-	policyDoc := roleOutput.Role.AssumeRolePolicyDocument
-	// Decode the policy document
-	decodedDocument, _ := url.PathUnescape(*policyDoc)
-	config.Debugf("CanAssumeRole policyDoc: %v", decodedDocument)
-	var policy struct {
-		Statement []struct {
-			Principal struct {
-				Service string `json:"Service"`
-			} `json:"Principal"`
-		} `json:"Statement"`
-	}
-	if err := json.Unmarshal([]byte(decodedDocument), &policy); err != nil {
-		config.Debugf("Unable to parse policy: %v", err)
-		return false, err
-	}
-
-	// Check if the service is allowed to assume the role
-	for _, stmt := range policy.Statement {
-		if stmt.Principal.Service == serviceName {
-			return true, nil
-		}
-	}
-
-	return false, nil
-}
diff --git a/internal/aws/kms/kms.go b/internal/aws/kms/kms.go
deleted file mode 100644
index e67fe978..00000000
--- a/internal/aws/kms/kms.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package kms
-
-import (
-	"context"
-	rainaws "github.com/aws-cloudformation/rain/internal/aws"
-	"github.com/aws/aws-sdk-go-v2/service/kms"
-	"time"
-)
-
-func getClient() *kms.Client {
-	return kms.NewFromConfig(rainaws.Config())
-}
-
-func IsKeyArnValid(keyArn string) bool {
-	client := getClient()
-
-	// Use the aws go sdk to call kms and make sure the key is valid
-	key, err := client.DescribeKey(context.Background(), &kms.DescribeKeyInput{KeyId: &keyArn})
-	if err != nil {
-		// Throws an error if it can't find the key
-		return false
-	}
-	if !key.KeyMetadata.Enabled {
-		return false
-	}
-	if key.KeyMetadata.ValidTo != nil {
-		// Make sure the key is not expired
-		if key.KeyMetadata.ValidTo.Before(time.Now()) {
-			return false
-		}
-	}
-	return true
-}
diff --git a/internal/aws/rds/rds.go b/internal/aws/rds/rds.go
index e8dc5632..e45cd85c 100644
--- a/internal/aws/rds/rds.go
+++ b/internal/aws/rds/rds.go
@@ -22,10 +22,6 @@ func GetValidEngineVersions(engine string) ([]string, error) {
 	}
 
 	for _, v := range res.DBEngineVersions {
-		if v.EngineVersion == nil {
-			// This should never happen
-			continue
-		}
 		retval = append(retval, *v.EngineVersion)
 	}
 
diff --git a/internal/aws/s3/s3.go b/internal/aws/s3/s3.go
index b3d7d80c..2d795b59 100644
--- a/internal/aws/s3/s3.go
+++ b/internal/aws/s3/s3.go
@@ -32,7 +32,7 @@ func getClient() *s3.Client {
 	return s3.NewFromConfig(aws.Config())
 }
 
-// BucketHasContents returns true if the bucket is not empty
+// Returns true if the bucket is not empty
 func BucketHasContents(bucketName string) (bool, error) {
 
 	res, err := getClient().ListObjectVersions(context.Background(),
@@ -68,6 +68,7 @@ func BucketExists(bucketName string) (bool, error) {
 func CreateBucket(bucketName string) error {
 	input := &s3.CreateBucketInput{
 		Bucket: ptr.String(bucketName),
+		ACL:    types.BucketCannedACLPrivate,
 	}
 
 	// We need a location constraint everywhere except us-east-1
@@ -141,7 +142,7 @@ func CreateBucket(bucketName string) error {
 	return err
 }
 
-// Upload uploads an artifact to the bucket with a unique name
+// Upload an artefact to the bucket with a unique name
 func Upload(bucketName string, content []byte) (string, error) {
 	isBucketExists, errBucketExists := BucketExists(bucketName)
 
@@ -158,6 +159,7 @@ func Upload(bucketName string, content []byte) (string, error) {
 	_, err := getClient().PutObject(context.Background(), &s3.PutObjectInput{
 		Bucket: ptr.String(bucketName),
 		Key:    ptr.String(key),
+		ACL:    types.ObjectCannedACLPrivate,
 		Body:   bytes.NewReader(content),
 	})
 
diff --git a/internal/aws/ssm/ssm.go b/internal/aws/ssm/ssm.go
deleted file mode 100644
index b82f785f..00000000
--- a/internal/aws/ssm/ssm.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package ssm
-
-import (
-	"context"
-	rainaws "github.com/aws-cloudformation/rain/internal/aws"
-	"github.com/aws/aws-sdk-go-v2/service/ssm"
-)
-
-func getClient() *ssm.Client {
-	return ssm.NewFromConfig(rainaws.Config())
-}
-
-// GetParameter returns the value of the specified parameter.
-func GetParameter(name string) (string, error) {
-	client := getClient()
-	parameter, err := client.GetParameter(context.Background(), &ssm.GetParameterInput{
-		Name: &name,
-	})
-	if err != nil {
-		return "", err
-	}
-
-	return *parameter.Parameter.Value, nil
-}
diff --git a/internal/cmd/build/interactive.go b/internal/cmd/build/interactive.go
index b2beb509..995f70b7 100644
--- a/internal/cmd/build/interactive.go
+++ b/internal/cmd/build/interactive.go
@@ -1,8 +1,6 @@
 package build
 
 import (
-	"fmt"
-	"os"
 	"strings"
 
 	"github.com/aws-cloudformation/rain/internal/aws/cfn"
@@ -150,56 +148,6 @@ func policy() {
 	}
 }
 
-func generic() {
-	// Choose the model
-	selections := []buildSelection{
-		{Name: HAIKU, Text: "Claude 3 Haiku"},
-		{Name: SONNET, Text: "Claude 3 Sonnet"},
-		{Name: OPUS, Text: "Claude 3 Opus"},
-	}
-	model = openPrompt("Choose a model", selections)
-
-	// Ask if the user wants to include a file
-	selections = []buildSelection{
-		{Name: YES, Text: "Yes"},
-		{Name: NO, Text: "No"},
-	}
-	yesno := openPrompt("Do you want to include the contents of a file with your question?", selections)
-
-	// Get the file content
-	var content string
-	if yesno == YES {
-		prompt := promptui.Prompt{
-			Label: "Enter the file path: ",
-		}
-		path, err := prompt.Run()
-		if err != nil {
-			panic(err)
-		}
-		fmt.Println("Including contents of file:", path)
-		contentBytes, err := os.ReadFile(path)
-		if err != nil {
-			panic(err)
-		}
-		content = string(contentBytes)
-	}
-
-	// Get the prompt
-	prompt := promptui.Prompt{
-		Label: "Prompt: ",
-	}
-	p, err := prompt.Run()
-	if err != nil {
-		panic(err)
-	}
-
-	if content != "" {
-		p += "\n\nReference the file contents below this line.\n\n" + content
-	}
-
-	promptGeneric(p, modelId(model))
-}
-
 // openPrompt uses promptui to show selections and returns what was selected
 func openPrompt(label string, selections []buildSelection) string {
 
@@ -253,7 +201,6 @@ func interactive() {
 		{Name: SCHEMA, Text: "Output the schema for a resource type"},
 		{Name: TEMPLATE, Text: "Create a CloudFormation template"},
 		{Name: POLICY, Text: "Create a policy validation file"},
-		{Name: GENERIC, Text: "Ask a bedrock model a generic question"},
 	}
 
 	label := "Entering build interactive mode... what would you like to do?"
@@ -272,9 +219,6 @@ func interactive() {
 	case POLICY:
 		policy()
 		return
-	case GENERIC:
-		generic()
-		return
 	}
 
 }
@@ -299,7 +243,4 @@ const (
 	HAIKU     = "claude3haiku"
 	GUARD     = "guard"
 	OPA       = "opa"
-	GENERIC   = "generic"
-	YES       = "yes"
-	NO        = "no"
 )
diff --git a/internal/cmd/build/prompt.go b/internal/cmd/build/prompt.go
index 90df20b5..5baa9e4d 100644
--- a/internal/cmd/build/prompt.go
+++ b/internal/cmd/build/prompt.go
@@ -169,26 +169,3 @@ func promptCfn(p string, mid string) {
 
 	}
 }
-
-func promptGeneric(p string, mid string) {
-
-	if isClaude2() {
-		config.Debugf("About to invoke bedrock %s with prompt: %s", mid, p)
-		r, err := bedrock.Invoke(p)
-		if err != nil {
-			panic(err)
-		}
-
-		fmt.Println(r)
-	} else {
-		system := "The conversation is likely to be about Amazon Web Services and Infrastructure as Code, but may be on any subject."
-		config.Debugf("About to invoke bedrock %s with system: %s, prompt: %s", mid, system, p)
-		r, err := bedrock.InvokeClaude3(p, mid, system)
-		if err != nil {
-			panic(err)
-		}
-
-		fmt.Println(r)
-
-	}
-}
diff --git a/internal/cmd/build/recommend.go b/internal/cmd/build/recommend.go
index fe6dc6d0..baa6992d 100644
--- a/internal/cmd/build/recommend.go
+++ b/internal/cmd/build/recommend.go
@@ -115,11 +115,6 @@ func recommend(args []string) {
 				{Name: "vpc", Text: "A VPC with 2 private and 2 public subnets"},
 			},
 		},
-		{Name: "ec2", Text: "EC2 Instances",
-			Sub: []reco{
-				{Name: "autoscaling", Text: "A VPC, AutoScaling Group, EC2 Launch Template, and ALB for a website"},
-			},
-		},
 	}
 
 	showPrompt(selections, "")
diff --git a/internal/cmd/build/tmpl/ec2/autoscaling.yaml b/internal/cmd/build/tmpl/ec2/autoscaling.yaml
deleted file mode 100644
index 41285a25..00000000
--- a/internal/cmd/build/tmpl/ec2/autoscaling.yaml
+++ /dev/null
@@ -1,246 +0,0 @@
-AWSTemplateFormatVersion: "2010-09-09"
-
-Description: |
-  Create a multi-az, load balanced and Auto Scaled sample web site running on
-  an Apache Web Server. The application is configured to span all
-  Availability Zones in the region and is Auto-Scaled based on the CPU
-  utilization of the web servers. Notifications will be sent to the operator
-  email address on scaling events. The instances are load balanced with a
-  simple health check against the default web page. **WARNING** This template
-  creates one or more Amazon EC2 instances and an Elastic Load Balancer. You
-  will be billed for the AWS resources used if you create a stack from this
-  template.
-
-Parameters:
-  InstanceType:
-    Description: WebServer EC2 instance type
-    Type: String
-    Default: t4g.micro
-
-  OperatorEMail:
-    Description: Email address to notify if there are any scaling operations
-    Type: String
-
-  LatestAmiId:
-    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
-    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64
-
-  KmsKeyArn:
-    Description: KMS Key ARN to encrypt data
-    Type: String
-
-  CertificateArn:
-    Description: Certificate ARN for HTTPS
-    Type: String
-
-Resources:
-
-  Network:
-    Type: !Rain::Module ../modules/vpc.yaml
-    Overrides:
-      VPC:
-        Properties:
-          Tags:
-            - Key: Name
-              Value: rain-build-autoscaling
-
-  ALB:
-    Type: !Rain::Module ../modules/load-balancer.yaml
-    Properties:
-      VPCId: !Ref NetworkVPC
-      PublicSubnet1: !Ref NetworkPublicSubnet1
-      PublicSubnet2: !Ref NetworkPublicSubnet2
-      DestinationSecurityGroupId: !GetAtt InstanceSecurityGroup.GroupId
-      CertificateArn: !Ref CertificateArn
-    Overrides:
-      LoadBalancer:
-        DependsOn:
-          - NetworkPublicSubnet1DefaultRoute
-          - NetworkPublicSubnet1RouteTableAssociation
-          - NetworkPublicSubnet2DefaultRoute
-          - NetworkPublicSubnet2RouteTableAssociation
-      TargetGroup:
-        Properties:
-          TargetType: instance
-
-  InstanceSecurityGroup:
-    Type: AWS::EC2::SecurityGroup
-    Properties:
-      GroupDescription: Enable HTTP from the load balancer only
-      SecurityGroupIngress:
-        - IpProtocol: tcp
-          FromPort: 80
-          ToPort: 80
-          SourceSecurityGroupId: !Ref ALBLoadBalancerSecurityGroup
-      VpcId: !Ref NetworkVPC
-
-  NotificationTopic:
-    Type: AWS::SNS::Topic
-    Properties:
-      DisplayName: !Sub ${AWS::StackName}-NotificationTopic
-      Subscription:
-        - Endpoint: !Ref OperatorEMail
-          Protocol: email
-      KmsMasterKeyId: !Ref KmsKeyArn
-
-  LaunchTemplate:
-    Type: AWS::EC2::LaunchTemplate
-    Metadata:
-      AWS::CloudFormation::Init:
-        config:
-          packages:
-            yum:
-              httpd: []
-          files:
-            /var/www/html/index.html:
-              content: |
-                  <html><body><h1>Congratulations, you have successfully launched the AWS CloudFormation sample.</h1></body></html>
-              mode: "000644"
-              owner: root
-              group: root
-            /etc/cfn/cfn-hup.conf:
-              content: !Sub |
-                [main]
-                stack=${AWS::StackId}
-                region=${AWS::Region}
-              mode: "000400"
-              owner: root
-              group: root
-            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
-              content: !Sub |
-                [cfn-auto-reloader-hook]
-                triggers=post.update
-                path=Resources.LaunchTemplate.Metadata.AWS::CloudFormation::Init
-                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchTemplate --region ${AWS::Region}
-                runas=root
-          services:
-            sysvinit:
-              httpd:
-                enabled: true
-                ensureRunning: true
-              cfn-hup:
-                enabled: true
-                ensureRunning: true
-                files:
-                  - /etc/cfn/cfn-hup.conf
-                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
-    Properties:
-      LaunchTemplateName: !Sub ${AWS::StackName}-LaunchTemplate
-      LaunchTemplateData:
-        ImageId: !Ref LatestAmiId
-        InstanceType: !Ref InstanceType
-        SecurityGroupIds: 
-          - !Ref InstanceSecurityGroup
-        BlockDeviceMappings:
-          - DeviceName: /dev/sda1
-            Ebs:
-              VolumeSize: 32
-        UserData: !Base64
-          Fn::Sub: |
-            #!/bin/bash
-            /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchTemplate --region ${AWS::Region}
-            /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerGroup --region ${AWS::Region}
-        TagSpecifications:
-          - ResourceType: instance
-            Tags:
-              - Key: Name
-                Value: !Sub ${AWS::StackName}-Instance
-
-  WebServerGroup:
-    Type: AWS::AutoScaling::AutoScalingGroup
-    CreationPolicy:
-      ResourceSignal:
-        Timeout: PT5M
-        Count: 1
-    UpdatePolicy:
-      AutoScalingRollingUpdate:
-        MinInstancesInService: 1
-        MaxBatchSize: 1
-        PauseTime: PT5M
-        WaitOnResourceSignals: true
-    DependsOn: NetworkVPCGW
-    Metadata:
-      cfn-lint:
-        config:
-          ignore_checks:
-            - E3014
-    Properties:
-      AvailabilityZones: 
-        - !Select [0, Fn::GetAZs: !Ref "AWS::Region"]
-        - !Select [1, Fn::GetAZs: !Ref "AWS::Region"]
-      LaunchTemplate:
-        LaunchTemplateId: !Ref LaunchTemplate
-        Version: !GetAtt LaunchTemplate.LatestVersionNumber
-      MinSize: "1"
-      MaxSize: "3"
-      TargetGroupARNs:
-        - !Ref ALBTargetGroup
-      NotificationConfigurations:
-        - TopicARN: !Ref NotificationTopic
-          NotificationTypes:
-            - autoscaling:EC2_INSTANCE_LAUNCH
-            - autoscaling:EC2_INSTANCE_LAUNCH_ERROR
-            - autoscaling:EC2_INSTANCE_TERMINATE
-            - autoscaling:EC2_INSTANCE_TERMINATE_ERROR
-      HealthCheckType: ELB
-      VPCZoneIdentifier: 
-        - !Ref NetworkPublicSubnet1
-        - !Ref NetworkPublicSubnet2
-
-  WebServerScaleUpPolicy:
-    Type: AWS::AutoScaling::ScalingPolicy
-    Properties:
-      AdjustmentType: ChangeInCapacity
-      AutoScalingGroupName: !Ref WebServerGroup
-      Cooldown: "60"
-      ScalingAdjustment: 1
-
-  WebServerScaleDownPolicy:
-    Type: AWS::AutoScaling::ScalingPolicy
-    Properties:
-      AdjustmentType: ChangeInCapacity
-      AutoScalingGroupName: !Ref WebServerGroup
-      Cooldown: "60"
-      ScalingAdjustment: -1
-
-  CPUAlarmHigh:
-    Type: AWS::CloudWatch::Alarm
-    Properties:
-      AlarmDescription: Scale-up if CPU > 90% for 10 minutes
-      MetricName: CPUUtilization
-      Namespace: AWS/EC2
-      Statistic: Average
-      Period: 300
-      EvaluationPeriods: 2
-      Threshold: 90
-      AlarmActions:
-        - !Ref WebServerScaleUpPolicy
-      Dimensions:
-        - Name: AutoScalingGroupName
-          Value: !Ref WebServerGroup
-      ComparisonOperator: GreaterThanThreshold
-
-  CPUAlarmLow:
-    Type: AWS::CloudWatch::Alarm
-    Properties:
-      AlarmDescription: Scale-down if CPU < 70% for 10 minutes
-      MetricName: CPUUtilization
-      Namespace: AWS/EC2
-      Statistic: Average
-      Period: 300
-      EvaluationPeriods: 2
-      Threshold: 70
-      AlarmActions:
-        - !Ref WebServerScaleDownPolicy
-      Dimensions:
-        - Name: AutoScalingGroupName
-          Value: !Ref WebServerGroup
-      ComparisonOperator: LessThanThreshold
-
-Outputs:
-  URL:
-    Description: The URL of the website
-    Value: !Join
-      - ""
-      - - https://
-        - !GetAtt ALBLoadBalancer.DNSName
diff --git a/internal/cmd/build/tmpl/modules/load-balancer.yaml b/internal/cmd/build/tmpl/modules/load-balancer.yaml
index 09c2892c..9822829b 100644
--- a/internal/cmd/build/tmpl/modules/load-balancer.yaml
+++ b/internal/cmd/build/tmpl/modules/load-balancer.yaml
@@ -94,7 +94,7 @@ Resources:
   TargetGroup:
     Type: AWS::ElasticLoadBalancingV2::TargetGroup
     Properties:
-      Port: 80
+      Port: 443
       Protocol: HTTP
       TargetGroupAttributes:
         - Key: deregistration_delay.timeout_seconds
diff --git a/internal/cmd/deploy/deploy.go b/internal/cmd/deploy/deploy.go
index 5ebd8c9d..f860d369 100644
--- a/internal/cmd/deploy/deploy.go
+++ b/internal/cmd/deploy/deploy.go
@@ -6,7 +6,6 @@ import (
 	"path/filepath"
 
 	"github.com/aws-cloudformation/rain/cft/format"
-	cftpkg "github.com/aws-cloudformation/rain/cft/pkg"
 	"github.com/aws-cloudformation/rain/internal/aws"
 	"github.com/aws-cloudformation/rain/internal/aws/cfn"
 	"github.com/aws-cloudformation/rain/internal/console"
@@ -29,7 +28,6 @@ var roleArn string
 var ignoreUnknownParams bool
 var noexec bool
 var changeset bool
-var experimental bool
 
 // Cmd is the deploy command's entrypoint
 var Cmd = &cobra.Command{
@@ -108,9 +106,6 @@ To list and delete changesets, use the ls and rm commands.
 			}
 
 			// Package template
-			if experimental {
-				cftpkg.Experimental = true
-			}
 			spinner.Push(fmt.Sprintf("Preparing template '%s'", base))
 			template := PackageTemplate(fn, yes)
 			spinner.Pop()
@@ -258,5 +253,4 @@ func init() {
 	Cmd.Flags().BoolVarP(&noexec, "no-exec", "x", false, "do not execute the changeset")
 	Cmd.Flags().BoolVar(&changeset, "changeset", false, "execute the changeset, rain deploy --changeset <stackName> <changeSetName>")
 	Cmd.Flags().StringVar(&format.NodeStyle, "node-style", "", format.NodeStyleDocs)
-	Cmd.Flags().BoolVar(&experimental, "experimental", false, "Acknowledge that you want to deploy with an experimental feature")
 }
diff --git a/internal/cmd/forecast/README.md b/internal/cmd/forecast/README.md
index 97ff16e2..67a72b4a 100644
--- a/internal/cmd/forecast/README.md
+++ b/internal/cmd/forecast/README.md
@@ -22,37 +22,20 @@ different role for the checks you make against the template.
 This command currently makes a few generic checks for a wide range of
 resources:
 
-- FG001: The resource already exists (for stack creation with hard coded resource
+- The resource already exists (for stack creation with hard coded resource
   names)
-- FG002: IAM permissions to interact with the resource. Keep in mind that this is a
-  slow operation and is disabled by default. You can enable it with the `--include-iam` argument. It is
+- IAM permissions to interact with the resource. Keep in mind that this is a
+  slow operation and can be suppressed with the `--skip-iam` argument. It is
   also not guaranteed to be 100% accurate, due to the difficulty with
   predicting the exact ARNs for all possible resources that are involved with
   the resource provider.
 
 ## Specific checks
 
-These can be ignored with the `--ignore` argument.
-
-| Code  | Description                                                                    |                                      
-|-------|--------------------------------------------------------------------------------|
-| F0001 | For a delete operation, the S3 bucket is not empty                             |
-| F0002 | S3 bucket policy has an invalid principal                                      |
-| F0003 | RDS cluster configuration is correct for the chosen engine                     |
-| F0004 | RDS monitoring role arn is correct                                             |
-| F0005 | RDS cluster quota is not at limit                                              |
-| F0006 | RDS instance configuration is correct for the chosen engine                    |
-| F0007 | EC2 instance and launch template KeyName exists                                |
-| F0008 | EC2 instance and launch template InstanceType exists                           |
-| F0009 | EC2 instance and launch template instance type and AMI match                   |
-| F0010 | Within the same template, are all security groups pointing to the same network |
-| F0011 | If there is no default VPC, does each security group have a vpc configured?    |
-| F0012 | Certificate not found for elastic load balancer                                |
-| F0013 | SNS Topic Key is valid                                                         |
-| F0014 | ELB target group Port and Protocol match                                       |
-| F0015 | ELB target groups must be of type instance if they are used by an ASG          |
-| F0016 | Lambda function role exists                                                    |
-| F0017 | Lambda function role can be assumed                                            |
+- For a delete operation, the S3 bucket is not empty
+- An S3 bucket policy has an invalid principal
+- Make sure RDS cluster configuration makes sense for the chosen engine 
+
 ## Estimates
 
 The forecast command also tries to estimate how long it thinks your stack will
@@ -66,6 +49,9 @@ You can view the issues list for the forecast command
 Please feel free to create an issue here whenever you get a stack failure that
 you think could have been prevented by one of these checks.
 
+In the near term, we're going to add the ability to suppress specific checks by
+giving each of them a code.
+
 Checks we plan to implement:
 
 - DynamoDB global table replica region requirements
@@ -77,6 +63,8 @@ Checks we plan to implement:
 - Function version does not exist
 - Warn on resource replacements for active traffic
 - API gateway account trust permission
+- Security group exists
+- Is an EC2 instance type available in the AZ
 
 
 
diff --git a/internal/cmd/forecast/autoscaling.go b/internal/cmd/forecast/autoscaling.go
deleted file mode 100644
index ac1ae280..00000000
--- a/internal/cmd/forecast/autoscaling.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package forecast
-
-/* TODO
-Resource handler returned message:
-The availability zones of the specified subnets and the
-Auto Scaling group do not match (Service: AutoScaling
-*/
-
-/* TODO
-Resource handler returned message: "Provided Target Group
-'arn:aws:elasticloadbalancing:us-east-1:755952356119:targetgroup/MyTargetGroup/bd36b34c22052e00'
-has invalid target type. Please ensure all provided Target Groups have target
-type of instance. (Service: AutoScaling
-
-This should probably be a cfn-lint check instead.
-
-*/
diff --git a/internal/cmd/forecast/codes.go b/internal/cmd/forecast/codes.go
deleted file mode 100644
index 8362dbc0..00000000
--- a/internal/cmd/forecast/codes.go
+++ /dev/null
@@ -1,28 +0,0 @@
-package forecast
-
-const (
-	FG001 = "FG001"
-	FG002 = "FG002"
-	F0001 = "F0001"
-	F0002 = "F0002"
-	F0003 = "F0003"
-	F0004 = "F0004"
-	F0005 = "F0005"
-	F0006 = "F0006"
-	F0007 = "F0007"
-	F0008 = "F0008"
-	F0009 = "F0009"
-	F0010 = "F0010"
-	F0011 = "F0011"
-	F0012 = "F0012"
-	F0013 = "F0013"
-	F0014 = "F0014"
-	F0015 = "F0015"
-	F0016 = "F0016"
-	F0017 = "F0017"
-	F0018 = "F0018"
-	F0019 = "F0019"
-	F0020 = "F0020"
-	F0021 = "F0021"
-	F0022 = "F0022"
-)
diff --git a/internal/cmd/forecast/ec2_instance.go b/internal/cmd/forecast/ec2_instance.go
index 22ffbc28..ed5a4d77 100644
--- a/internal/cmd/forecast/ec2_instance.go
+++ b/internal/cmd/forecast/ec2_instance.go
@@ -1,23 +1,21 @@
 package forecast
 
 import (
-	"fmt"
 	"github.com/aws-cloudformation/rain/internal/aws/ec2"
-	"github.com/aws-cloudformation/rain/internal/aws/ssm"
 	"github.com/aws-cloudformation/rain/internal/config"
 	"github.com/aws-cloudformation/rain/internal/console/spinner"
 	"github.com/aws-cloudformation/rain/internal/s11n"
 	"gopkg.in/yaml.v3"
-	"slices"
-	"strings"
 )
 
 func checkKeyName(input *PredictionInput, forecast *Forecast) {
 
 	var keyName string
 
-	props := getPropNode(input)
+	// Check to see if the resource has the KeyName property set
+	_, props, _ := s11n.GetMapValue(input.resource, "Properties")
 	if props == nil {
+		config.Debugf("expected %s to have Properties", input.logicalId)
 		return
 	}
 
@@ -40,11 +38,10 @@ func checkKeyName(input *PredictionInput, forecast *Forecast) {
 			spin(input.typeName, input.logicalId, "EC2 instance key exists?")
 
 			exists, _ := ec2.CheckKeyPairExists(keyName)
-			code := F0007
 			if exists {
-				forecast.Add(code, true, "Key exists")
+				forecast.Add(true, "Key exists")
 			} else {
-				forecast.Add(code, false, "Key does not exist")
+				forecast.Add(false, "Key does not exist")
 			}
 
 			spinner.Pop()
@@ -54,134 +51,15 @@ func checkKeyName(input *PredictionInput, forecast *Forecast) {
 	}
 }
 
-func resolveImageId(imageId string) string {
-	// Strings in CloudFormation can look like this:
-	// {{resolve:ssm:name}}
-	// Make an API call to systems manager parameter store to get the value
-
-	// TODO: This should probably move to where we resolve Refs
-
-	// Check to see if the image id starts with {{resolve:ssm:
-	if strings.HasPrefix(imageId, "{{resolve:ssm:") {
-		// The image id is a parameter name
-		imageId = strings.TrimPrefix(imageId, "{{resolve:ssm:")
-		imageId = strings.TrimSuffix(imageId, "}}")
-
-		config.Debugf("resolving %s", imageId)
-
-		resolved, err := ssm.GetParameter(imageId)
-		if err != nil {
-			config.Debugf("failed to resolve %s: %v", imageId, err)
-			return ""
-		}
-		config.Debugf("resolved %s to %s", imageId, resolved)
-		return resolved
-	}
-
-	// TODO: Secrets manager
-
-	return imageId
-}
-
-// checkInstanceType checks to see if the AMI and the instance type are compatible
-func checkInstanceType(input *PredictionInput, forecast *Forecast) {
-
-	var instanceType string
-	code := F0008
-
-	props := getPropNode(input)
-	if props == nil {
-		return
-	}
-
-	_, instanceTypeProp, _ := s11n.GetMapValue(props, "InstanceType")
-	if instanceTypeProp == nil {
-		config.Debugf("%s does not have InstanceType", input.logicalId)
-		return
-	}
-
-	// If the name is a Ref, resolve it
-	if instanceTypeProp.Kind == yaml.ScalarNode {
-		// The name is hard coded
-		instanceType = instanceTypeProp.Value
-	} else {
-		// We resolved Refs earlier so it should be a string
-		config.Debugf("%s.InstanceType is not a string", input.logicalId)
-		return
-	}
-
-	// Call the DescribeInstanceTypes API to get the instance type info
-	spin(input.typeName, input.logicalId, "EC2 instance type exists?")
-	instanceTypeInfo, err := ec2.GetInstanceType(instanceType)
-	if err != nil {
-		forecast.Add(code, false, err.Error())
-	} else {
-		forecast.Add(code, true, "Instance type exists")
-	}
-	spinner.Pop()
-
-	// Make sure the instance type and AMI agree
-
-	_, imageIdNode, _ := s11n.GetMapValue(props, "ImageId")
-	if imageIdNode == nil {
-		config.Debugf("%s does not have ImageId", input.logicalId)
-		return
-	}
-
-	imageId := resolveImageId(imageIdNode.Value)
-
-	spin(input.typeName, input.logicalId, "EC2 instance type matches AMI?")
-	image, err := ec2.GetImage(imageId)
-	if err != nil {
-		forecast.Add(F0009, false, fmt.Sprintf("Image not found: %s", imageId))
-		spinner.Pop()
-		return
-	}
-
-	instanceTypesForArch, err := ec2.GetInstanceTypesForArchitecture(string(image.Architecture))
-	if err != nil {
-		config.Debugf("failed to get instance types for architecture %s: %v", image.Architecture, err)
-		spinner.Pop()
-		return
-	}
-	code = F0009
-	if !slices.Contains(instanceTypesForArch, string(instanceTypeInfo.InstanceType)) {
-		forecast.Add(code, false, fmt.Sprintf("Instance type %s does not support AMI %s", instanceType, imageId))
-	} else {
-		forecast.Add(code, true, "Instance type matches AMI")
-	}
-	spinner.Pop()
-}
-
-func getPropNode(input *PredictionInput) *yaml.Node {
-	// Check to see if the resource has the InstanceType property set
-	_, props, _ := s11n.GetMapValue(input.resource, "Properties")
-	if props == nil {
-		config.Debugf("expected %s to have Properties", input.logicalId)
-		return nil
-	}
-
-	// If the input resource is an AWS::EC2::LaunchTemplate, props is LaunchTemplateData
-	if input.typeName == "AWS::EC2::LaunchTemplate" {
-		_, props, _ = s11n.GetMapValue(props, "LaunchTemplateData")
-		if props == nil {
-			config.Debugf("expected %s to have LaunchTemplateData", input.logicalId)
-			return nil
-		}
-	}
-	return props
-}
-
 func checkEC2Instance(input PredictionInput) Forecast {
 
+	// TODO - Is this instance type available in this AZ?
+
 	forecast := makeForecast(input.typeName, input.logicalId)
 
 	// Check to see if the key name exists
 	checkKeyName(&input, &forecast)
 
-	// Make sure the AMI and the instance type are compatible
-	checkInstanceType(&input, &forecast)
-
 	return forecast
 
 }
diff --git a/internal/cmd/forecast/ec2_launchtemplate.go b/internal/cmd/forecast/ec2_launchtemplate.go
deleted file mode 100644
index c8cbe629..00000000
--- a/internal/cmd/forecast/ec2_launchtemplate.go
+++ /dev/null
@@ -1,17 +0,0 @@
-package forecast
-
-// AWS::EC2::LaunchTemplate
-
-func checkEC2LaunchTemplate(input PredictionInput) Forecast {
-
-	forecast := makeForecast(input.typeName, input.logicalId)
-
-	// Check to see if the key name exists
-	checkKeyName(&input, &forecast)
-
-	// Make sure the AMI and the instance type are compatible
-	checkInstanceType(&input, &forecast)
-
-	return forecast
-
-}
diff --git a/internal/cmd/forecast/ec2_securitygroup.go b/internal/cmd/forecast/ec2_securitygroup.go
index 2ad140c2..00e26dad 100644
--- a/internal/cmd/forecast/ec2_securitygroup.go
+++ b/internal/cmd/forecast/ec2_securitygroup.go
@@ -1,20 +1,20 @@
 package forecast
 
-import (
-	"fmt"
-
-	"github.com/aws-cloudformation/rain/cft"
-	"github.com/aws-cloudformation/rain/internal/aws/ec2"
-	"github.com/aws-cloudformation/rain/internal/config"
-	"github.com/aws-cloudformation/rain/internal/console/spinner"
-	"github.com/aws-cloudformation/rain/internal/s11n"
-	"gopkg.in/yaml.v3"
-)
-
 func checkEC2SecurityGroup(input PredictionInput) Forecast {
 
 	// TODO - Cannot delete a security group that still has instances
 	// (See if there are instances not in this stack)
+	/* CLI commands
+
+	aws ec2 describe-network-interfaces --filters Name=group-id,Values=<group-id> --region <region> --output json
+
+	*/
+
+	//	filters := []types.Filter{}
+
+	//	input := *ec2.DescribeNetworkInterfacesInput{
+	//		Filters:
+	//	}
 
 	// TODO - Make sure security group exists in VPC
 	/*
@@ -30,113 +30,6 @@ func checkEC2SecurityGroup(input PredictionInput) Forecast {
 
 	forecast := makeForecast(input.typeName, input.logicalId)
 
-	spin(input.typeName, input.logicalId, "Checking if all security groups are in the same VPC")
-	checkSecurityGroupsInSameVPC(&input, &forecast)
-	spinner.Pop()
-
 	return forecast
 
 }
-
-// Not sure if we can accurately check if the security group is in use,
-// since the instances may come from this template and will be deleted first.
-// We could cross-check the physical IDs if it's actually an instance,
-// but more likely will be instances from an ASG and how do we correlate
-// them with the security group? Likely to have false positives.
-
-//// checkSecurityGroupIsInUse checks on deletes if a security group cannot be deleted
-//func checkSecurityGroupIsInUse(input *PredictionInput, forecast *Forecast) {
-//	if input.stackExists && action == DELETE {
-//		// Check if the security group is in use
-//		sgInUse, err := ec2.IsSecurityGroupInUse(input.stackName)
-//		if err != nil {
-//			forecast.Add(false, fmt.Sprintf("%v", err))
-//			return
-//		}
-//		if sgInUse {
-//			msg := fmt.Sprintf("Security group is in use by other resources")
-//			forecast.Add(false, msg)
-//			return
-//		}
-//		forecast.Add(true, "Security group is not in use by other resources")
-//	}
-//}
-
-func checkSecurityGroupsInSameVPC(input *PredictionInput, forecast *Forecast) {
-
-	code := F0010
-
-	// Get the default VPC
-	defaultVPCId, err := ec2.GetDefaultVPCId()
-	if err != nil {
-		msg := fmt.Sprintf("Unable to get default VPC Id: %v", err)
-		forecast.Add(code, false, msg)
-		return
-	}
-
-	config.Debugf("defaultVPCId: %s", defaultVPCId)
-
-	resources, err := input.source.GetSection(cft.Resources)
-	if err != nil {
-		config.Debugf("Unable to get Resources: %v", err)
-		return
-	}
-
-	var vpcId string
-
-	// Iterate through resources to find all security groups
-	for i := 0; i < len(resources.Content); i += 2 {
-
-		thisLogicalId := resources.Content[i].Value
-
-		_, typ, _ := s11n.GetMapValue(resources.Content[i+1], "Type")
-		if typ == nil {
-			config.Debugf("expected %s to have Type", resources.Content[i].Value)
-			continue
-		}
-		if typ.Value != "AWS::EC2::SecurityGroup" {
-			continue
-		}
-		_, props, _ := s11n.GetMapValue(resources.Content[i+1], "Properties")
-		if props == nil {
-			config.Debugf("expected %s to have Properties", resources.Content[i].Value)
-			continue
-		}
-
-		// See if VpcId is set. If not, assume it's the default.
-		// If it is hard coded or set in a param, make sure they are all the same
-		_, v, _ := s11n.GetMapValue(props, "VpcId")
-		var resourceVpcId string
-		if v != nil {
-			if v.Kind == yaml.ScalarNode {
-				resourceVpcId = v.Value
-			} else if v.Kind == yaml.MappingNode {
-				// Likely a Ref to the VPC created in this template
-				if v.Content[0].Value == "Ref" {
-					resourceVpcId = v.Content[1].Value
-				}
-			}
-			if resourceVpcId == "" {
-				// Not sure what this is
-				config.Debugf("Unable to determine VpcId for %s", input.logicalId)
-				return
-			}
-		} else {
-			resourceVpcId = defaultVPCId
-			if defaultVPCId == "" {
-				msg := fmt.Sprintf("There is no default VPC and VpcId is not set on %s", thisLogicalId)
-				forecast.Add(F0011, false, msg)
-			}
-		}
-
-		if vpcId == "" {
-			vpcId = resourceVpcId
-		} else if resourceVpcId != vpcId {
-			msg := fmt.Sprintf("VPC ID for this security group (%s) does not match %s", resourceVpcId, vpcId)
-			forecast.Add(code, false, msg)
-			return
-		}
-
-	}
-	forecast.Add(code, true, "All VPC Ids on security groups are the same")
-}
diff --git a/internal/cmd/forecast/elb.go b/internal/cmd/forecast/elb.go
deleted file mode 100644
index 2ea0a3bb..00000000
--- a/internal/cmd/forecast/elb.go
+++ /dev/null
@@ -1,129 +0,0 @@
-package forecast
-
-import (
-	"github.com/aws-cloudformation/rain/internal/aws/acm"
-	"github.com/aws-cloudformation/rain/internal/config"
-	"github.com/aws-cloudformation/rain/internal/console/spinner"
-	"github.com/aws-cloudformation/rain/internal/node"
-	"github.com/aws-cloudformation/rain/internal/s11n"
-	"gopkg.in/yaml.v3"
-)
-
-// Elastic Load Balancing
-
-// AWS::ElasticLoadBalancingV2::Listener
-
-func checkELBListener(input PredictionInput) Forecast {
-
-	forecast := makeForecast(input.typeName, input.logicalId)
-
-	// Resource handler returned message:
-	// "Certificate 'arn:aws:acm:us-east-1:X:certificate/Y'
-	//not found (Service: ElasticLoadBalancingV2
-	// (Expired certs cause this error)
-
-	// Get the certificate arn from props
-	_, props, _ := s11n.GetMapValue(input.resource, "Properties")
-	if props == nil {
-		config.Debugf("expected %s to have Properties", input.logicalId)
-		return forecast
-	}
-
-	_, certArns, _ := s11n.GetMapValue(props, "Certificates")
-	if certArns == nil {
-		config.Debugf("expected %s to have Certificates", input.logicalId)
-		return forecast
-	}
-	config.Debugf("Certificates: %s", node.ToSJson(certArns))
-
-	spin(input.typeName, input.logicalId, "Checking ELB Certs")
-
-	for _, certArnNode := range certArns.Content {
-		if len(certArnNode.Content) != 2 {
-			config.Debugf("Expected 2 children for certArnNode %s", node.ToSJson(certArnNode))
-			continue
-		}
-		certArn := certArnNode.Content[1].Value
-		config.Debugf("Checking certArn %s", certArn)
-		ok, err := acm.CheckCertificate(certArn)
-		if err != nil {
-			config.Debugf("Error checking certArn %s: %s", certArn, err)
-			spinner.Pop()
-			return forecast
-		}
-		code := F0012
-		if !ok {
-			forecast.Add(code, false, "Certificate not found or expired")
-		} else {
-			forecast.Add(code, true, "Certificate found")
-		}
-	}
-
-	spinner.Pop()
-
-	return forecast
-}
-
-func checkELBTargetGroup(input PredictionInput) Forecast {
-
-	forecast := makeForecast(input.typeName, input.logicalId)
-
-	// Check to make sure the Port and Protocol properties match
-	portNode := input.GetPropertyNode("Port")
-	protocolNode := input.GetPropertyNode("Protocol")
-	if portNode != nil && protocolNode != nil {
-		port := portNode.Value
-		protocol := protocolNode.Value
-		if port == "443" {
-			if protocol == "HTTPS" {
-				forecast.Add(F0014, true, "ELB target group port and protocol match")
-			} else {
-				forecast.Add(F0014, false, "ELB target group port and protocol do not match")
-			}
-		}
-		if port == "80" {
-			if protocol == "HTTP" {
-				forecast.Add(F0014, true, "ELB target group port and protocol match")
-			} else {
-				forecast.Add(F0014, false, "ELB target group port and protocol do not match")
-			}
-		}
-	}
-
-	targetTypeNode := input.GetPropertyNode("TargetType")
-	if targetTypeNode != nil {
-		targetType := targetTypeNode.Value
-		if targetType != "instance" {
-			// If this target group is being used by an ASG, the type must be instance
-			// Look at template resources to see if a launch template refers to this
-			autoscalingGroups := input.source.GetResourcesOfType("AWS::AutoScaling::AutoScalingGroup")
-			for _, asg := range autoscalingGroups {
-				_, props, _ := s11n.GetMapValue(asg, "Properties")
-				if props == nil {
-					continue
-				}
-				config.Debugf("Properties: %s", node.ToSJson(props))
-				_, targetGroupARNs, _ := s11n.GetMapValue(props, "TargetGroupARNs")
-				if targetGroupARNs == nil {
-					continue
-				}
-				for _, targetGroupArn := range targetGroupARNs.Content {
-					if targetGroupArn.Kind == yaml.MappingNode {
-						if targetGroupArn.Content[0].Kind == yaml.ScalarNode &&
-							targetGroupArn.Content[0].Value == "Ref" {
-							if targetGroupArn.Content[1].Value == input.logicalId {
-								forecast.Add(F0015, false,
-									"ELB target group must be of type instance if it is used by an ASG")
-							} else {
-								forecast.Add(F0015, true,
-									"ELB target group is of type instance if it is used by an ASG")
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-
-	return forecast
-}
diff --git a/internal/cmd/forecast/forecast.go b/internal/cmd/forecast/forecast.go
index afc5ce6e..343485c7 100644
--- a/internal/cmd/forecast/forecast.go
+++ b/internal/cmd/forecast/forecast.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"os"
 	"path/filepath"
-	"slices"
 	"strings"
 
 	"github.com/aws-cloudformation/rain/cft"
@@ -37,8 +36,8 @@ var Experimental bool
 // ResourceType is the resource type to check (optional --type to limit checks to one type)
 var ResourceType string
 
-// IncludeIAM indicates if we should perform permissions checks or not, to save time
-var IncludeIAM bool
+// SkipIAM indicates if we should perform permissions checks or not, to save time
+var SkipIAM bool
 
 // The optional parameters to use to create a change set for update predictions (--params)
 var params []string
@@ -52,19 +51,6 @@ var configFilePath string
 // Show success in addition to failures
 var all bool
 
-// Which stack action to check (--action)
-var action string
-
-// Which checks to ignore (--ignore)
-var ignore []string
-
-const (
-	ALL    = "all"
-	CREATE = "create"
-	UPDATE = "update"
-	DELETE = "delete"
-)
-
 type Env struct {
 	partition string
 	region    string
@@ -85,16 +71,6 @@ type PredictionInput struct {
 	roleArn     string
 }
 
-// GetPropertyNode returns the node for the given property name
-func (input *PredictionInput) GetPropertyNode(name string) *yaml.Node {
-	_, props, _ := s11n.GetMapValue(input.resource, "Properties")
-	if props != nil {
-		_, n, _ := s11n.GetMapValue(props, name)
-		return n
-	}
-	return nil
-}
-
 // LineNumber is the current line number in the template
 var LineNumber int
 
@@ -102,15 +78,8 @@ var LineNumber int
 type Forecast struct {
 	TypeName  string
 	LogicalId string
-	Passed    []Check
-	Failed    []Check
-}
-
-// Check is a specific check with a code that can be suppressed
-type Check struct {
-	Pass    bool
-	Code    string
-	Message string
+	Passed    []string
+	Failed    []string
 }
 
 func (f *Forecast) GetNumChecked() int {
@@ -131,42 +100,22 @@ func (f *Forecast) Append(forecast Forecast) {
 }
 
 // Add adds a pass or fail message, formatting it to include the type name and logical id
-func (f *Forecast) Add(code string, passed bool, message string) {
+func (f *Forecast) Add(passed bool, message string) {
 	msg := fmt.Sprintf("%v: %v %v - %v", LineNumber, f.TypeName, f.LogicalId, message)
-	check := Check{
-		Pass:    passed,
-		Code:    code,
-		Message: msg,
-	}
-
-	// If we are ignoring this check, don't add it to the forecast
-	if slices.Contains(ignore, code) || slices.Contains(ignore, f.TypeName) {
-		return
-	}
-
 	if passed {
-		f.Passed = append(f.Passed, check)
-	} else {
-		f.Failed = append(f.Failed, check)
-	}
-}
-
-func (c *Check) String() string {
-	var passFail string
-	if c.Pass {
-		passFail = "PASS"
+		f.Passed = append(f.Passed, msg)
 	} else {
-		passFail = "FAIL"
+		f.Failed = append(f.Failed, msg)
 	}
-	return fmt.Sprintf("%s %s on line %s", c.Code, passFail, c.Message)
+	// TODO - Do we want each failure to have a code so that it can be ignored?
 }
 
 func makeForecast(typeName string, logicalId string) Forecast {
 	return Forecast{
 		TypeName:  typeName,
 		LogicalId: logicalId,
-		Passed:    make([]Check, 0),
-		Failed:    make([]Check, 0),
+		Passed:    make([]string, 0),
+		Failed:    make([]string, 0),
 	}
 }
 
@@ -178,6 +127,40 @@ func spin(typeName string, logicalId string, message string) {
 	spinner.Push(fmt.Sprintf("%v %v - %v", typeName, logicalId, message))
 }
 
+// recurse over properties to resolve Refs
+func resolveParamRefs(name string, prop *yaml.Node, dc *dc.DeployConfig, parent *yaml.Node) {
+	if name == "Ref" && prop.Kind == yaml.ScalarNode {
+
+		for _, param := range dc.Params {
+			if *param.ParameterKey == prop.Value {
+				if parent.Kind == yaml.MappingNode {
+					// Replace the parent Mapping node
+					*parent = yaml.Node{Kind: yaml.ScalarNode, Value: *param.ParameterValue}
+				}
+				// would it be any other Kind?
+			}
+		}
+
+	} else if prop.Kind == yaml.MappingNode {
+		for i := 0; i < len(prop.Content); i += 2 {
+			resolveParamRefs(prop.Content[i].Value, prop.Content[i+1], dc, prop)
+		}
+	} else if prop.Kind == yaml.SequenceNode {
+		for _, p := range prop.Content {
+			resolveParamRefs("", p, dc, prop)
+		}
+	}
+}
+
+func resolveRefs(input PredictionInput) {
+	_, props, _ := s11n.GetMapValue(input.resource, "Properties")
+	if props != nil {
+		for i := 0; i < len(props.Content); i += 2 {
+			resolveParamRefs(props.Content[i].Value, props.Content[i+1], input.dc, props)
+		}
+	}
+}
+
 // Run all forecasters for the type
 func forecastForType(input PredictionInput) Forecast {
 
@@ -193,15 +176,15 @@ func forecastForType(input PredictionInput) Forecast {
 	// Resolve parameter refs
 	resolveRefs(input)
 
-	// Estimate how long the stackActionToEstimate will take
+	// Estimate how long the action will take
 	// (This is only for spinner output, we calculate total time separately)
-	var stackActionToEstimate StackAction
+	var action StackAction
 	if input.stackExists {
-		stackActionToEstimate = Update
+		action = Update
 	} else {
-		stackActionToEstimate = Create
+		action = Create
 	}
-	est, esterr := GetResourceEstimate(input.typeName, stackActionToEstimate)
+	est, esterr := GetResourceEstimate(input.typeName, action)
 	if esterr != nil {
 		config.Debugf("could not get estimate: %v", esterr)
 		est = 1
@@ -215,20 +198,19 @@ func forecastForType(input PredictionInput) Forecast {
 
 	spin(input.typeName, input.logicalId, "exists already?")
 
-	code := FG001
 	// Make sure the resource does not already exist
 	if cfn.ResourceAlreadyExists(input.typeName, input.resource,
 		input.stackExists, input.source.Node, input.dc) {
-		forecast.Add(code, false, "Already exists")
+		forecast.Add(false, "Already exists")
 	} else {
 		LineNumber = input.resource.Line
-		forecast.Add(code, true, "Does not exist")
+		forecast.Add(true, "Does not exist")
 	}
 
 	spinner.Pop()
 
 	// Check permissions
-	if IncludeIAM {
+	if !SkipIAM {
 		err := checkPermissions(input, &forecast)
 		if err != nil {
 			config.Debugf("Unable to check permissions: %v", err)
@@ -252,7 +234,6 @@ func forecastForType(input PredictionInput) Forecast {
 	if found {
 		// Call the prediction function and append the results
 		config.Debugf("Running forecaster for %v", input.typeName)
-		LineNumber = input.resource.Line
 		forecast.Append(fn(input))
 	}
 
@@ -272,6 +253,8 @@ func predict(source cft.Template, stackName string, stack types.Stack, stackExis
 	// Visit each resource in the template and see if it matches
 	// one of our predictions
 
+	// TODO: Create a changeset to evaluate updates
+
 	forecast := makeForecast("", "")
 
 	rootMap := source.Node.Content[0]
@@ -352,7 +335,7 @@ func predict(source cft.Template, stackName string, stack types.Stack, stackExis
 			forecast.GetNumFailed(),
 			forecast.GetNumChecked())))
 		for _, reason := range forecast.Failed {
-			fmt.Println(console.Red(reason.String()))
+			fmt.Println(console.Red(reason))
 		}
 		if all {
 			fmt.Println()
@@ -361,7 +344,7 @@ func predict(source cft.Template, stackName string, stack types.Stack, stackExis
 				forecast.GetNumPassed(),
 				forecast.GetNumChecked())))
 			for _, reason := range forecast.Passed {
-				fmt.Println(console.Green(reason.String()))
+				fmt.Println(console.Green(reason))
 			}
 		}
 
@@ -374,7 +357,7 @@ func predict(source cft.Template, stackName string, stack types.Stack, stackExis
 		if all {
 			fmt.Println()
 			for _, reason := range forecast.Passed {
-				fmt.Println(console.Green(reason.String()))
+				fmt.Println(console.Green(reason))
 			}
 		}
 		return true
@@ -403,8 +386,17 @@ This command is not a linter! Use cfn-lint for that. The forecast command
 is concerned with things that could go wrong during deployment, after the 
 template has been checked to make sure it has a valid syntax.
 
-This command checks for some common issues across all resources, and 
-resource-specific checks. See the README for more details.
+This command checks for some common issues across all resources:
+
+- The resource already exists
+- You do not have permissions to create/update/delete the resource
+- (More to come.. service quotas, drift issues)
+
+Resource-specific checks:
+
+- S3 bucket is not empty
+- S3 bucket policy has an invalid principal
+- (Many more to come...)
 `,
 	Args:                  cobra.RangeArgs(1, 2),
 	DisableFlagsInUseLine: true,
@@ -456,14 +448,6 @@ resource-specific checks. See the README for more details.
 		}
 		config.Debugf("Stack %v %v", stackName, msg)
 
-		if action == DELETE || action == UPDATE && !stackExists {
-			panic(fmt.Sprintf("stack %v does not exist, action %s is not valid", stackName, action))
-		}
-
-		if action == CREATE && stackExists {
-			panic(fmt.Sprintf("stack %v already exists, action %s is not valid", stackName, action))
-		}
-
 		dc, err := dc.GetDeployConfig(tags, params, configFilePath, base,
 			source, stack, stackExists, true, false)
 		if err != nil {
@@ -479,16 +463,15 @@ resource-specific checks. See the README for more details.
 
 func init() {
 	Cmd.Flags().BoolVar(&config.Debug, "debug", false, "Output debugging information")
-	Cmd.Flags().BoolVar(&IncludeIAM, "include-iam", false, "Include permissions checks, which can take a long time")
+	Cmd.Flags().BoolVar(&SkipIAM, "skip-iam", false, "Skip permissions checks, which can take a long time")
 	Cmd.Flags().BoolVarP(&all, "all", "a", false, "Show all checks, not just failed ones")
 	Cmd.Flags().BoolVarP(&Experimental, "experimental", "x", false, "Acknowledge that this is an experimental feature")
 	Cmd.Flags().StringVar(&RoleArn, "role-arn", "", "An optional execution role arn to use for predicting IAM failures")
+	// TODO - --op "create", "update", "delete", default: "all"
 	Cmd.Flags().StringVar(&ResourceType, "type", "", "Optional resource type to limit checks to only that type")
 	Cmd.Flags().StringSliceVar(&tags, "tags", []string{}, "add tags to the stack; use the format key1=value1,key2=value2")
 	Cmd.Flags().StringSliceVar(&params, "params", []string{}, "set parameter values; use the format key1=value1,key2=value2")
 	Cmd.Flags().StringVarP(&configFilePath, "config", "c", "", "YAML or JSON file to set tags and parameters")
-	Cmd.Flags().StringVar(&action, "action", ALL, "The stack action to check: create, update, delete, all (default is all)")
-	Cmd.Flags().StringSliceVar(&ignore, "ignore", []string{}, "Resource types and specific codes to ignore, separated by commas, for example, AWS::S3::Bucket,F0002")
 
 	// If you want to add a prediction for a type that is not already covered, add it here
 	// The function must return a Forecast struct
@@ -501,11 +484,6 @@ func init() {
 	forecasters["AWS::EC2::SecurityGroup"] = checkEC2SecurityGroup
 	forecasters["AWS::RDS::DBCluster"] = checkRDSDBCluster
 	forecasters["AWS::AutoScaling::LaunchConfiguration"] = checkAutoScalingLaunchConfiguration
-	forecasters["AWS::EC2::LaunchTemplate"] = checkEC2LaunchTemplate
-	forecasters["AWS::ElasticLoadBalancingV2::Listener"] = checkELBListener
-	forecasters["AWS::SNS::Topic"] = checkSNSTopic
-	forecasters["AWS::ElasticLoadBalancingV2::TargetGroup"] = checkELBTargetGroup
-	forecasters["AWS::Lambda::Function"] = checkLambdaFunction
 
 	// Initialize estimates map
 	InitEstimates()
diff --git a/internal/cmd/forecast/lambda.go b/internal/cmd/forecast/lambda.go
deleted file mode 100644
index 04a77feb..00000000
--- a/internal/cmd/forecast/lambda.go
+++ /dev/null
@@ -1,46 +0,0 @@
-package forecast
-
-import (
-	"github.com/aws-cloudformation/rain/internal/aws/iam"
-	"github.com/aws-cloudformation/rain/internal/config"
-	"github.com/aws-cloudformation/rain/internal/s11n"
-	"gopkg.in/yaml.v3"
-)
-
-// checkLambdaFunction checks for potential stack failures related to functions
-func checkLambdaFunction(input PredictionInput) Forecast {
-
-	forecast := makeForecast(input.typeName, input.logicalId)
-
-	_, props, _ := s11n.GetMapValue(input.resource, "Properties")
-	if props == nil {
-		config.Debugf("No Properties found for %s", input.logicalId)
-		return forecast
-	}
-	_, roleProp, _ := s11n.GetMapValue(props, "Role")
-
-	// If the role is specified, and it's a scalar, check if it exists
-	if roleProp != nil && roleProp.Kind == yaml.ScalarNode {
-		roleArn := roleProp.Value
-		LineNumber = roleProp.Line
-		if !iam.RoleExists(roleArn) {
-			forecast.Add(F0016, false, "Role does not exist")
-		} else {
-			forecast.Add(F0016, true, "Role exists")
-		}
-
-		// Check to make sure the iam role can be assumed by the lambda function
-		canAssume, err := iam.CanAssumeRole(roleArn, "lambda.amazonaws.com")
-		if err != nil {
-			config.Debugf("Error checking role: %s", err)
-		} else {
-			if !canAssume {
-				forecast.Add(F0017, false, "Role can not be assumed")
-			} else {
-				forecast.Add(F0017, true, "Role can be assumed")
-			}
-		}
-	}
-
-	return forecast
-}
diff --git a/internal/cmd/forecast/permissions.go b/internal/cmd/forecast/permissions.go
index c50307b7..d15c9000 100644
--- a/internal/cmd/forecast/permissions.go
+++ b/internal/cmd/forecast/permissions.go
@@ -90,33 +90,31 @@ func checkPermissions(input PredictionInput, forecast *Forecast) error {
 		return nil
 	}
 
-	code := FG002
-
 	var ok bool
 	var reason []string
 	if input.stackExists {
 		ok, reason = checkTypePermissions(input, resourceArn, "update")
 		if !ok {
-			forecast.Add(code, false,
+			forecast.Add(false,
 				fmt.Sprintf("Insufficient permissions to update %v\n\t%v", resourceArn, strings.Join(reason, "\n\t")))
 		} else {
-			forecast.Add(code, true, "Role has update permissions")
+			forecast.Add(true, "Role has update permissions")
 		}
 
 		ok, reason = checkTypePermissions(input, resourceArn, "delete")
 		if !ok {
-			forecast.Add(code, false,
+			forecast.Add(false,
 				fmt.Sprintf("Insufficient permissions to delete %v\n\t%v", resourceArn, strings.Join(reason, "\n\t")))
 		} else {
-			forecast.Add(code, true, "Role has delete permissions")
+			forecast.Add(true, "Role has delete permissions")
 		}
 	} else {
 		ok, reason = checkTypePermissions(input, resourceArn, "create")
 		if !ok {
-			forecast.Add(code, false,
+			forecast.Add(false,
 				fmt.Sprintf("Insufficient permissions to create %v\n\t%v", resourceArn, strings.Join(reason, "\n\t")))
 		} else {
-			forecast.Add(code, true, "Role has create permissions")
+			forecast.Add(true, "Role has create permissions")
 		}
 	}
 	return nil
diff --git a/internal/cmd/forecast/rds_dbcluster.go b/internal/cmd/forecast/rds_dbcluster.go
index a09c0ee4..6e833ad7 100644
--- a/internal/cmd/forecast/rds_dbcluster.go
+++ b/internal/cmd/forecast/rds_dbcluster.go
@@ -32,8 +32,6 @@ func checkRDSDBCluster(input PredictionInput) Forecast {
 
 	var clusterEngineVersion string
 
-	code := F0003
-
 	_, engine, _ := s11n.GetMapValue(props, "Engine")
 	_, engineVersion, _ := s11n.GetMapValue(props, "EngineVersion")
 	if engineVersion != nil {
@@ -60,21 +58,19 @@ func checkRDSDBCluster(input PredictionInput) Forecast {
 			if unexpected {
 				LineNumber = input.resource.Line
 				config.Debugf("db cluster resource: %s", node.ToJson(input.resource))
-				forecast.Add(code, false, fmt.Sprintf("unexpected EngineVersion: %s", engineVersion.Value))
+				forecast.Add(false, fmt.Sprintf("unexpected EngineVersion: %s", engineVersion.Value))
 			} else {
-				forecast.Add(code, true, "EngineVersion ok")
+				forecast.Add(true, "EngineVersion ok")
 			}
 		default:
 			config.Debugf("unexpected Engine value for %s: %s",
 				input.logicalId, engine.Value)
-			forecast.Add(code, false, "unexpected Engine value")
+			forecast.Add(false, "unexpected Engine value")
 		}
 	}
 
 	spinner.Pop()
 
-	code = F0004
-
 	spin(input.typeName, input.logicalId, "db cluster has MonitoringRoleARN?")
 
 	// Resource handler returned message: A MonitoringRoleARN value is required if you specify a MonitoringInterval value other than 0.
@@ -82,43 +78,41 @@ func checkRDSDBCluster(input PredictionInput) Forecast {
 	_, monitoringInterval, _ := s11n.GetMapValue(props, "MonitoringInterval")
 	if monitoringInterval != nil && monitoringInterval.Value != "0" {
 		if monitoringRoleARN == nil {
-			forecast.Add(code, false, "a MonitoringRoleARN value is required if you specify a MonitoringInterval value other than 0.")
+			forecast.Add(false, "a MonitoringRoleARN value is required if you specify a MonitoringInterval value other than 0.")
 		} else {
 			// Make sure the role actually exists
 			if monitoringRoleARN.Kind == yaml.ScalarNode &&
 				!iam.RoleExists(monitoringRoleARN.Value) {
-				forecast.Add(code, false,
+				forecast.Add(false,
 					fmt.Sprintf("MonitoringRoleARN not found: %s",
 						monitoringRoleARN.Value))
 			} else {
-				forecast.Add(code, true, "MonitoringRoleARN set")
+				forecast.Add(true, "MonitoringRoleARN set")
 			}
 		}
 	} else {
-		forecast.Add(code, true, "MonitoringInterval not set to something other than 0")
+		forecast.Add(true, "MonitoringInterval not set to something other than 0")
 	}
 
 	spinner.Pop()
 
-	code = F0005
-
 	spin(input.typeName, input.logicalId, "db clusters not at quota")
 
 	// Check to make sure we're not at quota
 	if !input.stackExists {
 		quota, err := servicequotas.GetQuota("rds", "L-952B80B8")
 		if err != nil {
-			forecast.Add(code, false, fmt.Sprintf("failed: %v", err))
+			forecast.Add(false, fmt.Sprintf("failed: %v", err))
 		} else {
 			// Get the number of clusters
 			numClusters, err := rds.GetNumClusters()
 			if err != nil {
-				forecast.Add(code, false, fmt.Sprintf("failed: %v", err))
+				forecast.Add(false, fmt.Sprintf("failed: %v", err))
 			} else {
 				if numClusters >= int(math.Round(quota)) {
-					forecast.Add(code, false, "already at quota for number of clusters")
+					forecast.Add(false, "already at quota for number of clusters")
 				} else {
-					forecast.Add(code, true,
+					forecast.Add(true,
 						fmt.Sprintf("quota for clusters ok: %v/%v",
 							numClusters, quota))
 				}
@@ -128,8 +122,6 @@ func checkRDSDBCluster(input PredictionInput) Forecast {
 
 	spinner.Pop()
 
-	code = F0006
-
 	// The engine version that you requested for your DB instance (a.b) does not match the engine version of your DB cluster (c.d)
 	// This kind of thing might be better in cfn-lint
 
@@ -161,11 +153,11 @@ func checkRDSDBCluster(input PredictionInput) Forecast {
 
 							instanceVersion := evNode.Value
 							if evNode.Kind == yaml.ScalarNode && instanceVersion != clusterEngineVersion {
-								forecast.Add(code, false, fmt.Sprintf(
+								forecast.Add(false, fmt.Sprintf(
 									"engine mismatch with %s: %s != %s",
 									logicalId, instanceVersion, clusterEngineVersion))
 							} else {
-								forecast.Add(code, true, "instance engine version matches")
+								forecast.Add(true, "instance engine version matches")
 							}
 						}
 					}
diff --git a/internal/cmd/forecast/resolve.go b/internal/cmd/forecast/resolve.go
deleted file mode 100644
index d28aca08..00000000
--- a/internal/cmd/forecast/resolve.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package forecast
-
-import (
-	"github.com/aws-cloudformation/rain/internal/aws/ssm"
-	"github.com/aws-cloudformation/rain/internal/config"
-	"github.com/aws-cloudformation/rain/internal/dc"
-	"github.com/aws-cloudformation/rain/internal/s11n"
-	"gopkg.in/yaml.v3"
-	"strings"
-)
-
-// recurse over properties to resolve Refs
-func resolveParamRefs(name string, prop *yaml.Node, dc *dc.DeployConfig, parent *yaml.Node) {
-	if name == "Ref" && prop.Kind == yaml.ScalarNode {
-
-		for _, param := range dc.Params {
-			if *param.ParameterKey == prop.Value {
-				if parent.Kind == yaml.MappingNode {
-
-					var val string
-
-					// Resolve SSM types like AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
-					if param.ResolvedValue != nil {
-						// Will this ever not be nil? Maybe for updates?
-						val = *param.ResolvedValue
-					} else {
-						val = *param.ParameterValue
-						// We don't have the param type here...
-						if strings.HasPrefix(val, "/aws/service/") {
-							// Assume this is an SSM parameter
-							resolved, err := ssm.GetParameter(val)
-							if err != nil {
-								config.Debugf("could not get SSM parameter: %v", err)
-							} else {
-								val = resolved
-							}
-						}
-					}
-
-					// Replace the parent Mapping node
-					*parent = yaml.Node{Kind: yaml.ScalarNode, Value: val}
-				}
-				// would it be any other Kind?
-			}
-		}
-
-	} else if prop.Kind == yaml.MappingNode {
-		for i := 0; i < len(prop.Content); i += 2 {
-			resolveParamRefs(prop.Content[i].Value, prop.Content[i+1], dc, prop)
-		}
-	} else if prop.Kind == yaml.SequenceNode {
-		for _, p := range prop.Content {
-			resolveParamRefs("", p, dc, prop)
-		}
-	}
-}
-
-func resolveRefs(input PredictionInput) {
-	_, props, _ := s11n.GetMapValue(input.resource, "Properties")
-	if props != nil {
-		for i := 0; i < len(props.Content); i += 2 {
-			resolveParamRefs(props.Content[i].Value, props.Content[i+1], input.dc, props)
-		}
-	}
-}
diff --git a/internal/cmd/forecast/s3_bucket.go b/internal/cmd/forecast/s3_bucket.go
index 85e9afe0..59d7a0b8 100644
--- a/internal/cmd/forecast/s3_bucket.go
+++ b/internal/cmd/forecast/s3_bucket.go
@@ -18,10 +18,6 @@ func checkBucketNotEmpty(input PredictionInput, bucket *types.StackResourceDetai
 		return true, "Stack does not exist"
 	}
 
-	if bucket == nil {
-		return false, "bucket is nil"
-	}
-
 	spin(input.typeName, input.logicalId, "bucket not empty?")
 
 	config.Debugf("Checking if the bucket %v is not empty", *bucket.PhysicalResourceId)
@@ -75,11 +71,10 @@ func checkS3Bucket(input PredictionInput) Forecast {
 			config.Debugf("Physical bucket name is: %v", bucketName)
 
 			empty, reason := checkBucketNotEmpty(input, res)
-			code := F0001
 			if !empty {
-				forecast.Add(code, false, reason)
+				forecast.Add(false, reason)
 			} else {
-				forecast.Add(code, true, "Bucket is empty")
+				forecast.Add(true, "Bucket is empty")
 			}
 		}
 	} else {
diff --git a/internal/cmd/forecast/s3_bucketpolicy.go b/internal/cmd/forecast/s3_bucketpolicy.go
index 05c1728d..0edce837 100644
--- a/internal/cmd/forecast/s3_bucketpolicy.go
+++ b/internal/cmd/forecast/s3_bucketpolicy.go
@@ -38,17 +38,15 @@ func checkS3BucketPolicy(input PredictionInput) Forecast {
 		if policyDocument != nil {
 			res, err := iam.CheckPolicyDocument(policyDocument)
 
-			code := F0002
-
 			if err != nil {
-				forecast.Add(code, false, fmt.Sprintf("Unable to check policy document: %v", err))
+				forecast.Add(false, fmt.Sprintf("Unable to check policy document: %v", err))
 			}
 
 			if !res {
 				LineNumber = policyDocument.Line
-				forecast.Add(code, false, "Invalid principal in policy document")
+				forecast.Add(false, "Invalid principal in policy document")
 			} else {
-				forecast.Add(code, true, "Principal is valid")
+				forecast.Add(true, "Principal is valid")
 			}
 		}
 	}
diff --git a/internal/cmd/forecast/sns_topic.go b/internal/cmd/forecast/sns_topic.go
deleted file mode 100644
index 43a8e8b8..00000000
--- a/internal/cmd/forecast/sns_topic.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package forecast
-
-import (
-	"github.com/aws-cloudformation/rain/internal/aws/kms"
-	"github.com/aws-cloudformation/rain/internal/console/spinner"
-)
-
-func checkSNSTopic(input PredictionInput) Forecast {
-
-	forecast := makeForecast(input.typeName, input.logicalId)
-
-	spin(input.typeName, input.logicalId, "Checking SNS Topic Key")
-	checkSNSTopicKey(&input, &forecast)
-	spinner.Pop()
-
-	return forecast
-}
-
-func checkSNSTopicKey(input *PredictionInput, forecast *Forecast) {
-
-	// Get the KmsMasterKeyId from the input resource properties
-	k := input.GetPropertyNode("KmsMasterKeyId")
-	if k != nil {
-		keyArn := k.Value
-		valid := kms.IsKeyArnValid(keyArn)
-		if valid {
-			forecast.Add(F0013, true, "KMS Key is valid")
-		} else {
-			forecast.Add(F0013, false, "KMS Key is invalid")
-		}
-	}
-}
diff --git a/internal/cmd/logs/logs.go b/internal/cmd/logs/logs.go
index 79b13fff..00602a78 100644
--- a/internal/cmd/logs/logs.go
+++ b/internal/cmd/logs/logs.go
@@ -13,6 +13,7 @@ var allLogs = false
 var chart = false
 var logsLength uint
 var logsDays uint
+var sinceUserInitiated = false
 
 // Cmd is the logs command's entrypoint
 var Cmd = &cobra.Command{
@@ -63,4 +64,5 @@ func init() {
 	Cmd.Flags().BoolVar(&config.Debug, "debug", false, "Output debugging information")
 	Cmd.Flags().UintVarP(&logsLength, "length", "l", 0, "Number of logs to display")
 	Cmd.Flags().UintVarP(&logsDays, "days", "d", 0, "Age of the logs to display in days")
+	Cmd.Flags().BoolVarP(&sinceUserInitiated, "since-user-initiated", "s", false, "Only show logs since the last 'User Initiated' event")
 }
diff --git a/internal/cmd/logs/util.go b/internal/cmd/logs/util.go
index 0602c40e..ea84d2d0 100644
--- a/internal/cmd/logs/util.go
+++ b/internal/cmd/logs/util.go
@@ -150,6 +150,19 @@ func getLogs(stackName, resourceName string) (events, error) {
 		logs[i], logs[j] = logs[j], logs[i]
 	}
 
+	// Filter out logs since last user initiated event
+	if sinceUserInitiated {
+		newLogs = make([]types.StackEvent, 0)
+		for _, log := range logs {
+			isLatestUserInitiated := log.ResourceStatusReason != nil && *log.ResourceStatusReason == "User Initiated"
+			newLogs = append(newLogs, log)
+			if isLatestUserInitiated {
+				break
+			}
+		}
+		logs = newLogs
+	}
+
 	spinner.Pop()
 
 	return logs, nil
diff --git a/internal/cmd/ls/changeset.go b/internal/cmd/ls/changeset.go
index b050ba58..a847e3a5 100644
--- a/internal/cmd/ls/changeset.go
+++ b/internal/cmd/ls/changeset.go
@@ -43,9 +43,6 @@ func showChangeset(stackName, changeSetName string) {
 		panic(ui.Errorf(err, "failed to get changeset '%s'", changeSetName))
 	}
 	config.Debugf("ChangeSet response: %+v", cs)
-	if cs == nil {
-		return
-	}
 	out := ""
 	out += fmt.Sprintf("Arn: %v\n", *cs.ChangeSetId)
 	out += fmt.Sprintf("Created: %v\n", cs.CreationTime)
diff --git a/internal/dc/dc.go b/internal/dc/dc.go
index 35066033..0a4a0f58 100644
--- a/internal/dc/dc.go
+++ b/internal/dc/dc.go
@@ -1,4 +1,4 @@
-// Package dc contains types and functions to facilitate parsing
+// The dc package contains types and functions to facilitate parsing
 // user-supplied configuration like tags and parameters, which
 // are used for the deploy and forecast commands
 package dc
@@ -21,10 +21,8 @@ import (
 )
 
 type configFileFormat struct {
-	Parameters      map[string]string `yaml:"Parameters"`
-	Tags            map[string]string `yaml:"Tags"`
-	LowerParameters map[string]string `yaml:"parameters,omitempty"`
-	LowerTags       map[string]string `yaml:"tags,omitempty"`
+	Parameters map[string]string `yaml:"Parameters"`
+	Tags       map[string]string `yaml:"Tags"`
 }
 
 // DeployConfig represents the user-supplied configuration for a deployment
@@ -251,16 +249,8 @@ func GetDeployConfig(
 			panic(ui.Errorf(err, "unable to parse yaml in '%s'", configFilePath))
 		}
 
-		config.Debugf("Parsed config file struct: %+v", configFile)
-
 		combinedTags = configFile.Tags
-		if len(combinedTags) == 0 && len(configFile.LowerTags) > 0 {
-			combinedTags = configFile.LowerTags
-		}
 		combinedParameters = configFile.Parameters
-		if len(combinedParameters) == 0 && len(configFile.LowerParameters) > 0 {
-			combinedParameters = configFile.LowerParameters
-		}
 
 		for k, v := range parsedTagFlag {
 			if _, ok := combinedTags[k]; ok {
diff --git a/internal/table/table.go b/internal/table/table.go
index affb5f19..6e60b54b 100644
--- a/internal/table/table.go
+++ b/internal/table/table.go
@@ -6,7 +6,7 @@
 // I liked this package but needed to make some breaking changes
 // ==============================================================
 //
-// Columns are left-aligned and padded to accommodate the largest cell in that
+// Columns are left-aligned and padded to accomodate the largest cell in that
 // column.
 //
 // Source: https://github.com/rodaine/table
@@ -75,7 +75,7 @@ type Formatter func(string, ...interface{}) string
 // A WidthFunc calculates the width of a string. By default, the number of runes
 // is used but this may not be appropriate for certain character sets. The
 // package runewidth (https://github.com/mattn/go-runewidth) could be used to
-// accommodate multi-cell characters (such as emoji or CJK characters).
+// accomodate multi-cell characters (such as emoji or CJK characters).
 type WidthFunc func(string) int
 
 type row struct {
@@ -313,12 +313,12 @@ func (t *table) lenOffset(s string, w int) string {
 	return strings.Repeat(" ", l)
 }
 
-//func max(i1, i2 int) int {
-//	if i1 > i2 {
-//		return i1
-//	}
-//	return i2
-//}
+func max(i1, i2 int) int {
+	if i1 > i2 {
+		return i1
+	}
+	return i2
+}
 
 func safeOffset(sarr []string, idx int) string {
 	if idx >= len(sarr) {


Test patch:
diff --git a/internal/aws/iam/iam_test.go b/internal/aws/iam/iam_test.go
index ceec07b4..f996f88b 100644
--- a/internal/aws/iam/iam_test.go
+++ b/internal/aws/iam/iam_test.go
@@ -10,14 +10,3 @@ func TestTransformCallerArn2(t *testing.T) {
 		t.Errorf("Failed to transform IAM user type arn")
 	}
 }
-
-func TestGetRoleNameFromArn(t *testing.T) {
-	roleArn := "arn:aws:iam::755952356119:role/my-role"
-	roleName, err := GetRoleNameFromArn(roleArn)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if roleName != "my-role" {
-		t.Errorf("Failed to get role name from arn: %s", roleArn)
-	}
-}
diff --git a/internal/cmd/forecast/forecast_test.go b/internal/cmd/forecast/forecast_test.go
index 0a6ab67e..3a206818 100644
--- a/internal/cmd/forecast/forecast_test.go
+++ b/internal/cmd/forecast/forecast_test.go
@@ -4,8 +4,8 @@ import "testing"
 
 func TestForecastMethods(t *testing.T) {
 	forecast := makeForecast("A::B::C", "Id")
-	forecast.Add("CODE1", true, "Succeeded")
-	forecast.Add("CODE2", false, "Failed")
+	forecast.Add(true, "Succeeded")
+	forecast.Add(false, "Failed")
 
 	if forecast.GetNumChecked() != 2 {
 		t.Errorf("Expected 2 checks")
@@ -20,7 +20,7 @@ func TestForecastMethods(t *testing.T) {
 	}
 
 	f2 := makeForecast("D::E::F", "Id2")
-	f2.Add("CODE2", false, "f2 fail")
+	f2.Add(false, "f2 fail")
 	forecast.Append(f2)
 
 	if forecast.GetNumChecked() != 3 || forecast.GetNumFailed() != 2 {
diff --git a/internal/cmd/logs/logs_test.go b/internal/cmd/logs/logs_test.go
index 8e450a60..3720a246 100644
--- a/internal/cmd/logs/logs_test.go
+++ b/internal/cmd/logs/logs_test.go
@@ -26,10 +26,11 @@ func Example_logs_help() {
 	//   logs, log
 	//
 	// Flags:
-	//   -a, --all           include uninteresting logs
-	//   -c, --chart         Output a gantt chart of the most recent action as an html file
-	//   -d, --days uint     Age of the logs to display in days
-	//       --debug         Output debugging information
-	//   -h, --help          help for logs
-	//   -l, --length uint   Number of logs to display
+	//   -a, --all                    include uninteresting logs
+	//   -c, --chart                  Output a gantt chart of the most recent action as an html file
+	//   -d, --days uint              Age of the logs to display in days
+	//       --debug                  Output debugging information
+	//   -h, --help                   help for logs
+	//   -l, --length uint            Number of logs to display
+	//   -s, --since-user-initiated   Only show logs since the last 'User Initiated' event
 }
diff --git a/internal/cmd/tree/tree_test.go b/internal/cmd/tree/tree_test.go
index ef6c5e83..f0e85855 100644
--- a/internal/cmd/tree/tree_test.go
+++ b/internal/cmd/tree/tree_test.go
@@ -23,27 +23,3 @@ func Example_tree() {
 	//       Parameters:
 	//         - BucketName
 }
-
-func Example_sub() {
-
-	os.Args = []string{
-		os.Args[0],
-		"../../../test/templates/fix-320.yaml",
-	}
-
-	console.NoColour = true
-
-	tree.Cmd.Execute()
-	// Output:
-	// Resources:
-	//   BackupSelection:
-	//     DependsOn:
-	//       Parameters:
-	//         - AWS::AccountId
-	//         - AWS::Partition
-	//         - AWS::Region
-	//         - AWS::StackName
-	//         - BackupPlanId
-	//       Resources:
-	//         - EfsFileSystem
-}
diff --git a/test/templates/build/ecs/cluster.yaml b/test/templates/build/ecs/cluster.yaml
index 493d357f..26b6bb81 100644
--- a/test/templates/build/ecs/cluster.yaml
+++ b/test/templates/build/ecs/cluster.yaml
@@ -434,7 +434,7 @@ Resources:
   ELBTargetGroup:
     Type: AWS::ElasticLoadBalancingV2::TargetGroup
     Properties:
-      Port: 80
+      Port: 443
       Protocol: HTTP
       TargetGroupAttributes:
         - Key: deregistration_delay.timeout_seconds
diff --git a/test/templates/fix-320.yaml b/test/templates/fix-320.yaml
deleted file mode 100644
index 03b1d496..00000000
--- a/test/templates/fix-320.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-Parameters:
-  BackupPlanId:
-    Type: String
-
-Resources:
-  BackupSelection:
-    Type: AWS::Backup::BackupSelection
-    Properties:
-      BackupPlanId: !Ref BackupPlanId
-      BackupSelection:
-        IamRoleArn: !Sub arn:${AWS::Partition}:iam::${AWS::AccountId}:role/service-role/AWSBackupDefaultServiceRole
-        Resources:
-          - !Sub arn:${AWS::Partition}:elasticfilesystem:${AWS::Region}:${AWS::AccountId}:file-system/${EfsFileSystem}
-        SelectionName: !Sub ${AWS::StackName}
-
-  EfsFileSystem:
-    Type: AWS::EFS::FileSystem
-
diff --git a/test/templates/forecast/autoscaling-fail.yaml b/test/templates/forecast/autoscaling-fail.yaml
deleted file mode 100644
index 48e1c8ee..00000000
--- a/test/templates/forecast/autoscaling-fail.yaml
+++ /dev/null
@@ -1,496 +0,0 @@
-AWSTemplateFormatVersion: "2010-09-09"
-
-Description: |
-  Create a multi-az, load balanced and Auto Scaled sample web site running on
-  an Apache Web Server. The application is configured to span all
-  Availability Zones in the region and is Auto-Scaled based on the CPU
-  utilization of the web servers. Notifications will be sent to the operator
-  email address on scaling events. The instances are load balanced with a
-  simple health check against the default web page. **WARNING** This template
-  creates one or more Amazon EC2 instances and an Elastic Load Balancer. You
-  will be billed for the AWS resources used if you create a stack from this
-  template.
-
-Parameters:
-  InstanceType:
-    Description: WebServer EC2 instance type
-    Type: String
-    Default: t4g.micro
-
-  OperatorEMail:
-    Description: Email address to notify if there are any scaling operations
-    Type: String
-
-  LatestAmiId:
-    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
-    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64
-
-  KmsKeyArn:
-    Description: KMS Key ARN to encrypt data
-    Type: String
-
-  CertificateArn:
-    Description: Certificate ARN for HTTPS
-    Type: String
-
-Resources:
-  InstanceSecurityGroup:
-    Type: AWS::EC2::SecurityGroup
-    Metadata:
-      Comment: Without VpcId this defaults to the default VPC which is not NetworkVPC
-    Properties:
-      GroupDescription: Enable HTTP from the load balancer only
-      SecurityGroupIngress:
-        - IpProtocol: tcp
-          FromPort: 80
-          ToPort: 80
-          SourceSecurityGroupId: !Ref ALBLoadBalancerSecurityGroup
-
-  NotificationTopic:
-    Type: AWS::SNS::Topic
-    Metadata:
-      Comment: If the key arn is not valid the ASG will fail to create
-    Properties:
-      DisplayName: !Sub ${AWS::StackName}-NotificationTopic
-      Subscription:
-        - Endpoint: !Ref OperatorEMail
-          Protocol: email
-      KmsMasterKeyId: !Ref KmsKeyArn
-
-  LaunchTemplate:
-    Type: AWS::EC2::LaunchTemplate
-    Metadata:
-      AWS::CloudFormation::Init:
-        config:
-          packages:
-            yum:
-              httpd: []
-          files:
-            /var/www/html/index.html:
-              content: |
-                <html><body><h1>Congratulations, you have successfully launched the AWS CloudFormation sample.</h1></body></html>
-              mode: "000644"
-              owner: root
-              group: root
-            /etc/cfn/cfn-hup.conf:
-              content: !Sub |
-                [main]
-                stack=${AWS::StackId}
-                region=${AWS::Region}
-              mode: "000400"
-              owner: root
-              group: root
-            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
-              content: !Sub |
-                [cfn-auto-reloader-hook]
-                triggers=post.update
-                path=Resources.LaunchTemplate.Metadata.AWS::CloudFormation::Init
-                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchTemplate --region ${AWS::Region}
-                runas=root
-          services:
-            sysvinit:
-              httpd:
-                enabled: true
-                ensureRunning: true
-              cfn-hup:
-                enabled: true
-                ensureRunning: true
-                files:
-                  - /etc/cfn/cfn-hup.conf
-                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
-    Properties:
-      LaunchTemplateName: !Sub ${AWS::StackName}-LaunchTemplate
-      LaunchTemplateData:
-        ImageId: !Ref LatestAmiId
-        InstanceType: !Ref InstanceType
-        SecurityGroupIds:
-          - !Ref InstanceSecurityGroup
-        BlockDeviceMappings:
-          - DeviceName: /dev/sda1
-            Ebs:
-              VolumeSize: 32
-        UserData: !Base64
-          Fn::Sub: |
-            #!/bin/bash
-            /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchTemplate --region ${AWS::Region}
-            /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerGroup --region ${AWS::Region}
-        TagSpecifications:
-          - ResourceType: instance
-            Tags:
-              - Key: Name
-                Value: !Sub ${AWS::StackName}-Instance
-
-  WebServerGroup:
-    Type: AWS::AutoScaling::AutoScalingGroup
-    CreationPolicy:
-      ResourceSignal:
-        Timeout: PT5M
-        Count: 1
-    UpdatePolicy:
-      AutoScalingRollingUpdate:
-        MinInstancesInService: 1
-        MaxBatchSize: 1
-        PauseTime: PT5M
-        WaitOnResourceSignals: true
-    DependsOn: NetworkVPCGW
-    Metadata:
-      cfn-lint:
-        config:
-          ignore_checks:
-            - E3014
-    Properties:
-      AvailabilityZones:
-        - !Select
-          - 0
-          - !GetAZs
-            Ref: AWS::Region
-        - !Select
-          - 1
-          - !GetAZs
-            Ref: AWS::Region
-      LaunchTemplate:
-        LaunchTemplateId: !Ref LaunchTemplate
-        Version: !GetAtt LaunchTemplate.LatestVersionNumber
-      MinSize: "1"
-      MaxSize: "3"
-      TargetGroupARNs:
-        - !Ref ALBTargetGroup
-      NotificationConfigurations:
-        - TopicARN: !Ref NotificationTopic
-          NotificationTypes:
-            - autoscaling:EC2_INSTANCE_LAUNCH
-            - autoscaling:EC2_INSTANCE_LAUNCH_ERROR
-            - autoscaling:EC2_INSTANCE_TERMINATE
-            - autoscaling:EC2_INSTANCE_TERMINATE_ERROR
-      HealthCheckType: ELB
-      VPCZoneIdentifier:
-        - !Ref NetworkPublicSubnet1
-        - !Ref NetworkPublicSubnet2
-
-  WebServerScaleUpPolicy:
-    Type: AWS::AutoScaling::ScalingPolicy
-    Properties:
-      AdjustmentType: ChangeInCapacity
-      AutoScalingGroupName: !Ref WebServerGroup
-      Cooldown: "60"
-      ScalingAdjustment: 1
-
-  WebServerScaleDownPolicy:
-    Type: AWS::AutoScaling::ScalingPolicy
-    Properties:
-      AdjustmentType: ChangeInCapacity
-      AutoScalingGroupName: !Ref WebServerGroup
-      Cooldown: "60"
-      ScalingAdjustment: -1
-
-  CPUAlarmHigh:
-    Type: AWS::CloudWatch::Alarm
-    Properties:
-      AlarmDescription: Scale-up if CPU > 90% for 10 minutes
-      MetricName: CPUUtilization
-      Namespace: AWS/EC2
-      Statistic: Average
-      Period: 300
-      EvaluationPeriods: 2
-      Threshold: 90
-      AlarmActions:
-        - !Ref WebServerScaleUpPolicy
-      Dimensions:
-        - Name: AutoScalingGroupName
-          Value: !Ref WebServerGroup
-      ComparisonOperator: GreaterThanThreshold
-
-  CPUAlarmLow:
-    Type: AWS::CloudWatch::Alarm
-    Properties:
-      AlarmDescription: Scale-down if CPU < 70% for 10 minutes
-      MetricName: CPUUtilization
-      Namespace: AWS/EC2
-      Statistic: Average
-      Period: 300
-      EvaluationPeriods: 2
-      Threshold: 70
-      AlarmActions:
-        - !Ref WebServerScaleDownPolicy
-      Dimensions:
-        - Name: AutoScalingGroupName
-          Value: !Ref WebServerGroup
-      ComparisonOperator: LessThanThreshold
-
-  NetworkVPC:
-    Type: AWS::EC2::VPC
-    Properties:
-      CidrBlock: 10.0.0.0/16
-      EnableDnsHostnames: true
-      EnableDnsSupport: true
-      InstanceTenancy: default
-      Tags:
-        - Key: Name
-          Value: rain-build-autoscaling-fail
-
-  NetworkPublicSubnet1:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 0
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.0.0/18
-      MapPublicIpOnLaunch: true
-      VpcId: !Ref NetworkVPC
-    Metadata:
-      guard:
-        SuppressedRules:
-          - SUBNET_AUTO_ASSIGN_PUBLIC_IP_DISABLED
-
-  NetworkPublicSubnet1RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPublicSubnet1RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPublicSubnet1RouteTable
-      SubnetId: !Ref NetworkPublicSubnet1
-
-  NetworkPublicSubnet1DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      GatewayId: !Ref NetworkInternetGateway
-      RouteTableId: !Ref NetworkPublicSubnet1RouteTable
-    Metadata:
-      guard:
-        SuppressedRules:
-          - NO_UNRESTRICTED_ROUTE_TO_IGW
-    DependsOn:
-      - NetworkVPCGW
-
-  NetworkPublicSubnet1EIP:
-    Type: AWS::EC2::EIP
-    Properties:
-      Domain: vpc
-
-  NetworkPublicSubnet1NATGateway:
-    Type: AWS::EC2::NatGateway
-    Properties:
-      AllocationId: !GetAtt NetworkPublicSubnet1EIP.AllocationId
-      SubnetId: !Ref NetworkPublicSubnet1
-    DependsOn:
-      - NetworkPublicSubnet1DefaultRoute
-      - NetworkPublicSubnet1RouteTableAssociation
-
-  NetworkPublicSubnet2:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 1
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.64.0/18
-      MapPublicIpOnLaunch: true
-      VpcId: !Ref NetworkVPC
-    Metadata:
-      guard:
-        SuppressedRules:
-          - SUBNET_AUTO_ASSIGN_PUBLIC_IP_DISABLED
-
-  NetworkPublicSubnet2RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPublicSubnet2RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPublicSubnet2RouteTable
-      SubnetId: !Ref NetworkPublicSubnet2
-
-  NetworkPublicSubnet2DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      GatewayId: !Ref NetworkInternetGateway
-      RouteTableId: !Ref NetworkPublicSubnet2RouteTable
-    Metadata:
-      guard:
-        SuppressedRules:
-          - NO_UNRESTRICTED_ROUTE_TO_IGW
-    DependsOn:
-      - NetworkVPCGW
-
-  NetworkPublicSubnet2EIP:
-    Type: AWS::EC2::EIP
-    Properties:
-      Domain: vpc
-
-  NetworkPublicSubnet2NATGateway:
-    Type: AWS::EC2::NatGateway
-    Properties:
-      AllocationId: !GetAtt NetworkPublicSubnet2EIP.AllocationId
-      SubnetId: !Ref NetworkPublicSubnet2
-    DependsOn:
-      - NetworkPublicSubnet2DefaultRoute
-      - NetworkPublicSubnet2RouteTableAssociation
-
-  NetworkPrivateSubnet1Subnet:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 0
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.128.0/18
-      MapPublicIpOnLaunch: false
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet1RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet1RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPrivateSubnet1RouteTable
-      SubnetId: !Ref NetworkPrivateSubnet1Subnet
-
-  NetworkPrivateSubnet1DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      NatGatewayId: !Ref NetworkPublicSubnet1NATGateway
-      RouteTableId: !Ref NetworkPrivateSubnet1RouteTable
-
-  NetworkPrivateSubnet2Subnet:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 1
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.192.0/18
-      MapPublicIpOnLaunch: false
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet2RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet2RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPrivateSubnet2RouteTable
-      SubnetId: !Ref NetworkPrivateSubnet2Subnet
-
-  NetworkPrivateSubnet2DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      NatGatewayId: !Ref NetworkPublicSubnet2NATGateway
-      RouteTableId: !Ref NetworkPrivateSubnet2RouteTable
-
-  NetworkInternetGateway:
-    Type: AWS::EC2::InternetGateway
-
-  NetworkVPCGW:
-    Type: AWS::EC2::VPCGatewayAttachment
-    Properties:
-      InternetGatewayId: !Ref NetworkInternetGateway
-      VpcId: !Ref NetworkVPC
-
-  ALBLoadBalancer:
-    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
-    Properties:
-      LoadBalancerAttributes:
-        - Key: deletion_protection.enabled
-          Value: false
-        - Key: routing.http.drop_invalid_header_fields.enabled
-          Value: true
-      Scheme: internet-facing
-      SecurityGroups:
-        - !GetAtt ALBLoadBalancerSecurityGroup.GroupId
-      Subnets:
-        - !Ref NetworkPublicSubnet1
-        - !Ref NetworkPublicSubnet2
-      Type: application
-    Metadata:
-      checkov:
-        skip:
-          - id: CKV_AWS_91
-      guard:
-        SuppressedRules:
-          - ELB_DELETION_PROTECTION_ENABLED
-    DependsOn:
-      - NetworkPublicSubnet1DefaultRoute
-      - NetworkPublicSubnet1RouteTableAssociation
-      - NetworkPublicSubnet2DefaultRoute
-      - NetworkPublicSubnet2RouteTableAssociation
-
-  ALBLoadBalancerSecurityGroup:
-    Type: AWS::EC2::SecurityGroup
-    Properties:
-      GroupDescription: Automatically created Security Group for ELB
-      SecurityGroupIngress:
-        - CidrIp: 0.0.0.0/0
-          Description: Allow from anyone on port 443
-          FromPort: 443
-          IpProtocol: tcp
-          ToPort: 443
-      VpcId: !Ref NetworkVPC
-
-  ALBLoadBalancerEgress:
-    Type: AWS::EC2::SecurityGroupEgress
-    Properties:
-      Description: Load balancer to target
-      DestinationSecurityGroupId: !GetAtt InstanceSecurityGroup.GroupId
-      FromPort: 80
-      GroupId: !GetAtt ALBLoadBalancerSecurityGroup.GroupId
-      IpProtocol: tcp
-      ToPort: 80
-
-  ALBLoadBalancerListener:
-    Type: AWS::ElasticLoadBalancingV2::Listener
-    Properties:
-      DefaultActions:
-        - TargetGroupArn: !Ref ALBTargetGroup
-          Type: forward
-      LoadBalancerArn: !Ref ALBLoadBalancer
-      Port: 443
-      Protocol: HTTPS
-      Certificates:
-        - CertificateArn: !Ref CertificateArn
-      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
-    Metadata:
-      guard:
-        SuppressedRules:
-          - ELBV2_ACM_CERTIFICATE_REQUIRED
-
-  ALBTargetGroup:
-    Type: AWS::ElasticLoadBalancingV2::TargetGroup
-    Metadata:
-      Comment: | 
-        The Port and Protocol should match, 
-        and the target type should be instance if this group
-        is being used with a launch template
-    Properties:
-      Port: 80
-      Protocol: HTTP
-      TargetGroupAttributes:
-        - Key: deregistration_delay.timeout_seconds
-          Value: "10"
-        - Key: stickiness.enabled
-          Value: "false"
-      TargetType: ip
-      VpcId: !Ref NetworkVPC
-
-Outputs:
-  URL:
-    Description: The URL of the website
-    Value: !Join
-      - ""
-      - - https://
-        - !GetAtt ALBLoadBalancer.DNSName
-
diff --git a/test/templates/forecast/autoscaling-succeed.yaml b/test/templates/forecast/autoscaling-succeed.yaml
deleted file mode 100644
index 8f180821..00000000
--- a/test/templates/forecast/autoscaling-succeed.yaml
+++ /dev/null
@@ -1,496 +0,0 @@
-AWSTemplateFormatVersion: "2010-09-09"
-
-Description: |
-  Create a multi-az, load balanced and Auto Scaled sample web site running on
-  an Apache Web Server. The application is configured to span all
-  Availability Zones in the region and is Auto-Scaled based on the CPU
-  utilization of the web servers. Notifications will be sent to the operator
-  email address on scaling events. The instances are load balanced with a
-  simple health check against the default web page. **WARNING** This template
-  creates one or more Amazon EC2 instances and an Elastic Load Balancer. You
-  will be billed for the AWS resources used if you create a stack from this
-  template.
-
-Parameters:
-  InstanceType:
-    Description: WebServer EC2 instance type
-    Type: String
-    Default: t4g.micro
-
-  OperatorEMail:
-    Description: Email address to notify if there are any scaling operations
-    Type: String
-
-  LatestAmiId:
-    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
-    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64
-
-  KmsKeyArn:
-    Description: KMS Key ARN to encrypt data
-    Type: String
-
-  CertificateArn:
-    Description: Certificate ARN for HTTPS
-    Type: String
-
-Resources:
-  InstanceSecurityGroup:
-    Type: AWS::EC2::SecurityGroup
-    Properties:
-      GroupDescription: Enable HTTP from the load balancer only
-      SecurityGroupIngress:
-        - IpProtocol: tcp
-          FromPort: 80
-          ToPort: 80
-          SourceSecurityGroupId: !Ref ALBLoadBalancerSecurityGroup
-      VpcId: !Ref NetworkVPC
-
-  NotificationTopic:
-    Type: AWS::SNS::Topic
-    Properties:
-      DisplayName: !Sub ${AWS::StackName}-NotificationTopic
-      Subscription:
-        - Endpoint: !Ref OperatorEMail
-          Protocol: email
-      KmsMasterKeyId: !Ref KmsKeyArn
-
-  LaunchTemplate:
-    Type: AWS::EC2::LaunchTemplate
-    Metadata:
-      AWS::CloudFormation::Init:
-        config:
-          packages:
-            yum:
-              httpd: []
-          files:
-            /var/www/html/index.html:
-              content: |
-                <html><body><h1>Congratulations, you have successfully launched the AWS CloudFormation sample.</h1></body></html>
-              mode: "000644"
-              owner: root
-              group: root
-            /etc/cfn/cfn-hup.conf:
-              content: !Sub |
-                [main]
-                stack=${AWS::StackId}
-                region=${AWS::Region}
-              mode: "000400"
-              owner: root
-              group: root
-            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
-              content: !Sub |
-                [cfn-auto-reloader-hook]
-                triggers=post.update
-                path=Resources.LaunchTemplate.Metadata.AWS::CloudFormation::Init
-                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchTemplate --region ${AWS::Region}
-                runas=root
-          services:
-            sysvinit:
-              httpd:
-                enabled: true
-                ensureRunning: true
-              cfn-hup:
-                enabled: true
-                ensureRunning: true
-                files:
-                  - /etc/cfn/cfn-hup.conf
-                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
-    Properties:
-      LaunchTemplateName: !Sub ${AWS::StackName}-LaunchTemplate
-      LaunchTemplateData:
-        ImageId: !Ref LatestAmiId
-        InstanceType: !Ref InstanceType
-        SecurityGroupIds:
-          - !Ref InstanceSecurityGroup
-        BlockDeviceMappings:
-          - DeviceName: /dev/sda1
-            Ebs:
-              VolumeSize: 32
-        UserData: !Base64
-          Fn::Sub: |
-            #!/bin/bash
-            /opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource LaunchTemplate --region ${AWS::Region}
-            /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WebServerGroup --region ${AWS::Region}
-        TagSpecifications:
-          - ResourceType: instance
-            Tags:
-              - Key: Name
-                Value: !Sub ${AWS::StackName}-Instance
-
-  WebServerGroup:
-    Type: AWS::AutoScaling::AutoScalingGroup
-    CreationPolicy:
-      ResourceSignal:
-        Timeout: PT5M
-        Count: 1
-    UpdatePolicy:
-      AutoScalingRollingUpdate:
-        MinInstancesInService: 1
-        MaxBatchSize: 1
-        PauseTime: PT5M
-        WaitOnResourceSignals: true
-    DependsOn: NetworkVPCGW
-    Metadata:
-      cfn-lint:
-        config:
-          ignore_checks:
-            - E3014
-    Properties:
-      AvailabilityZones:
-        - !Select
-          - 0
-          - !GetAZs
-            Ref: AWS::Region
-        - !Select
-          - 1
-          - !GetAZs
-            Ref: AWS::Region
-      LaunchTemplate:
-        LaunchTemplateId: !Ref LaunchTemplate
-        Version: !GetAtt LaunchTemplate.LatestVersionNumber
-      MinSize: "1"
-      MaxSize: "3"
-      TargetGroupARNs:
-        - !Ref ALBTargetGroup
-      NotificationConfigurations:
-        - TopicARN: !Ref NotificationTopic
-          NotificationTypes:
-            - autoscaling:EC2_INSTANCE_LAUNCH
-            - autoscaling:EC2_INSTANCE_LAUNCH_ERROR
-            - autoscaling:EC2_INSTANCE_TERMINATE
-            - autoscaling:EC2_INSTANCE_TERMINATE_ERROR
-      HealthCheckType: ELB
-      VPCZoneIdentifier:
-        - !Ref NetworkPublicSubnet1
-        - !Ref NetworkPublicSubnet2
-
-  WebServerScaleUpPolicy:
-    Type: AWS::AutoScaling::ScalingPolicy
-    Properties:
-      AdjustmentType: ChangeInCapacity
-      AutoScalingGroupName: !Ref WebServerGroup
-      Cooldown: "60"
-      ScalingAdjustment: 1
-
-  WebServerScaleDownPolicy:
-    Type: AWS::AutoScaling::ScalingPolicy
-    Properties:
-      AdjustmentType: ChangeInCapacity
-      AutoScalingGroupName: !Ref WebServerGroup
-      Cooldown: "60"
-      ScalingAdjustment: -1
-
-  CPUAlarmHigh:
-    Type: AWS::CloudWatch::Alarm
-    Properties:
-      AlarmDescription: Scale-up if CPU > 90% for 10 minutes
-      MetricName: CPUUtilization
-      Namespace: AWS/EC2
-      Statistic: Average
-      Period: 300
-      EvaluationPeriods: 2
-      Threshold: 90
-      AlarmActions:
-        - !Ref WebServerScaleUpPolicy
-      Dimensions:
-        - Name: AutoScalingGroupName
-          Value: !Ref WebServerGroup
-      ComparisonOperator: GreaterThanThreshold
-
-  CPUAlarmLow:
-    Type: AWS::CloudWatch::Alarm
-    Properties:
-      AlarmDescription: Scale-down if CPU < 70% for 10 minutes
-      MetricName: CPUUtilization
-      Namespace: AWS/EC2
-      Statistic: Average
-      Period: 300
-      EvaluationPeriods: 2
-      Threshold: 70
-      AlarmActions:
-        - !Ref WebServerScaleDownPolicy
-      Dimensions:
-        - Name: AutoScalingGroupName
-          Value: !Ref WebServerGroup
-      ComparisonOperator: LessThanThreshold
-
-  NetworkVPC:
-    Type: AWS::EC2::VPC
-    Properties:
-      CidrBlock: 10.0.0.0/16
-      EnableDnsHostnames: true
-      EnableDnsSupport: true
-      InstanceTenancy: default
-      Tags:
-        - Key: Name
-          Value: rain-build-autoscaling-succeed
-
-  NetworkPublicSubnet1:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 0
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.0.0/18
-      MapPublicIpOnLaunch: true
-      VpcId: !Ref NetworkVPC
-    Metadata:
-      guard:
-        SuppressedRules:
-          - SUBNET_AUTO_ASSIGN_PUBLIC_IP_DISABLED
-
-  NetworkPublicSubnet1RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPublicSubnet1RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPublicSubnet1RouteTable
-      SubnetId: !Ref NetworkPublicSubnet1
-
-  NetworkPublicSubnet1DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      GatewayId: !Ref NetworkInternetGateway
-      RouteTableId: !Ref NetworkPublicSubnet1RouteTable
-    Metadata:
-      guard:
-        SuppressedRules:
-          - NO_UNRESTRICTED_ROUTE_TO_IGW
-    DependsOn:
-      - NetworkVPCGW
-
-  NetworkPublicSubnet1EIP:
-    Type: AWS::EC2::EIP
-    Properties:
-      Domain: vpc
-
-  NetworkPublicSubnet1NATGateway:
-    Type: AWS::EC2::NatGateway
-    Properties:
-      AllocationId: !GetAtt NetworkPublicSubnet1EIP.AllocationId
-      SubnetId: !Ref NetworkPublicSubnet1
-    DependsOn:
-      - NetworkPublicSubnet1DefaultRoute
-      - NetworkPublicSubnet1RouteTableAssociation
-
-  NetworkPublicSubnet2:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 1
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.64.0/18
-      MapPublicIpOnLaunch: true
-      VpcId: !Ref NetworkVPC
-    Metadata:
-      guard:
-        SuppressedRules:
-          - SUBNET_AUTO_ASSIGN_PUBLIC_IP_DISABLED
-
-  NetworkPublicSubnet2RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPublicSubnet2RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPublicSubnet2RouteTable
-      SubnetId: !Ref NetworkPublicSubnet2
-
-  NetworkPublicSubnet2DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      GatewayId: !Ref NetworkInternetGateway
-      RouteTableId: !Ref NetworkPublicSubnet2RouteTable
-    Metadata:
-      guard:
-        SuppressedRules:
-          - NO_UNRESTRICTED_ROUTE_TO_IGW
-    DependsOn:
-      - NetworkVPCGW
-
-  NetworkPublicSubnet2EIP:
-    Type: AWS::EC2::EIP
-    Properties:
-      Domain: vpc
-
-  NetworkPublicSubnet2NATGateway:
-    Type: AWS::EC2::NatGateway
-    Properties:
-      AllocationId: !GetAtt NetworkPublicSubnet2EIP.AllocationId
-      SubnetId: !Ref NetworkPublicSubnet2
-    DependsOn:
-      - NetworkPublicSubnet2DefaultRoute
-      - NetworkPublicSubnet2RouteTableAssociation
-
-  NetworkPrivateSubnet1Subnet:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 0
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.128.0/18
-      MapPublicIpOnLaunch: false
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet1RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet1RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPrivateSubnet1RouteTable
-      SubnetId: !Ref NetworkPrivateSubnet1Subnet
-
-  NetworkPrivateSubnet1DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      NatGatewayId: !Ref NetworkPublicSubnet1NATGateway
-      RouteTableId: !Ref NetworkPrivateSubnet1RouteTable
-
-  NetworkPrivateSubnet2Subnet:
-    Type: AWS::EC2::Subnet
-    Properties:
-      AvailabilityZone: !Select
-        - 1
-        - !GetAZs
-          Ref: AWS::Region
-      CidrBlock: 10.0.192.0/18
-      MapPublicIpOnLaunch: false
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet2RouteTable:
-    Type: AWS::EC2::RouteTable
-    Properties:
-      VpcId: !Ref NetworkVPC
-
-  NetworkPrivateSubnet2RouteTableAssociation:
-    Type: AWS::EC2::SubnetRouteTableAssociation
-    Properties:
-      RouteTableId: !Ref NetworkPrivateSubnet2RouteTable
-      SubnetId: !Ref NetworkPrivateSubnet2Subnet
-
-  NetworkPrivateSubnet2DefaultRoute:
-    Type: AWS::EC2::Route
-    Properties:
-      DestinationCidrBlock: 0.0.0.0/0
-      NatGatewayId: !Ref NetworkPublicSubnet2NATGateway
-      RouteTableId: !Ref NetworkPrivateSubnet2RouteTable
-
-  NetworkInternetGateway:
-    Type: AWS::EC2::InternetGateway
-
-  NetworkVPCGW:
-    Type: AWS::EC2::VPCGatewayAttachment
-    Properties:
-      InternetGatewayId: !Ref NetworkInternetGateway
-      VpcId: !Ref NetworkVPC
-
-  ALBLoadBalancer:
-    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
-    Properties:
-      LoadBalancerAttributes:
-        - Key: deletion_protection.enabled
-          Value: false
-        - Key: routing.http.drop_invalid_header_fields.enabled
-          Value: true
-      Scheme: internet-facing
-      SecurityGroups:
-        - !GetAtt ALBLoadBalancerSecurityGroup.GroupId
-      Subnets:
-        - !Ref NetworkPublicSubnet1
-        - !Ref NetworkPublicSubnet2
-      Type: application
-
-      # Need these... but can't put them in the module
-      # They will need to be overrides in the parent which is not ideal
-      #DependsOn:
-      #  - PublicSubnet1DefaultRoute
-      #  - PublicSubnet1RouteTableAssociation
-      #  - PublicSubnet2DefaultRoute
-      #  - PublicSubnet2RouteTableAssociation
-    Metadata:
-      checkov:
-        skip:
-          - id: CKV_AWS_91
-      guard:
-        SuppressedRules:
-          - ELB_DELETION_PROTECTION_ENABLED
-    DependsOn:
-      - NetworkPublicSubnet1DefaultRoute
-      - NetworkPublicSubnet1RouteTableAssociation
-      - NetworkPublicSubnet2DefaultRoute
-      - NetworkPublicSubnet2RouteTableAssociation
-
-  ALBLoadBalancerSecurityGroup:
-    Type: AWS::EC2::SecurityGroup
-    Properties:
-      GroupDescription: Automatically created Security Group for ELB
-      SecurityGroupIngress:
-        - CidrIp: 0.0.0.0/0
-          Description: Allow from anyone on port 443
-          FromPort: 443
-          IpProtocol: tcp
-          ToPort: 443
-      VpcId: !Ref NetworkVPC
-
-  ALBLoadBalancerEgress:
-    Type: AWS::EC2::SecurityGroupEgress
-    Properties:
-      Description: Load balancer to target
-      DestinationSecurityGroupId: !GetAtt InstanceSecurityGroup.GroupId
-      FromPort: 80
-      GroupId: !GetAtt ALBLoadBalancerSecurityGroup.GroupId
-      IpProtocol: tcp
-      ToPort: 80
-
-  ALBLoadBalancerListener:
-    Type: AWS::ElasticLoadBalancingV2::Listener
-    Properties:
-      DefaultActions:
-        - TargetGroupArn: !Ref ALBTargetGroup
-          Type: forward
-      LoadBalancerArn: !Ref ALBLoadBalancer
-      Port: 443
-      Protocol: HTTPS
-      Certificates:
-        - CertificateArn: !Ref CertificateArn
-      SslPolicy: ELBSecurityPolicy-TLS13-1-2-2021-06
-    Metadata:
-      guard:
-        SuppressedRules:
-          - ELBV2_ACM_CERTIFICATE_REQUIRED
-
-  ALBTargetGroup:
-    Type: AWS::ElasticLoadBalancingV2::TargetGroup
-    Properties:
-      Port: 80
-      Protocol: HTTP
-      TargetGroupAttributes:
-        - Key: deregistration_delay.timeout_seconds
-          Value: "10"
-        - Key: stickiness.enabled
-          Value: "false"
-      TargetType: instance
-      VpcId: !Ref NetworkVPC
-
-Outputs:
-  URL:
-    Description: The URL of the website
-    Value: !Join
-      - ""
-      - - https://
-        - !GetAtt ALBLoadBalancer.DNSName
-
diff --git a/test/templates/forecast/ec2-instance-fail.yaml b/test/templates/forecast/ec2-instance-fail.yaml
deleted file mode 100644
index 71e0c79b..00000000
--- a/test/templates/forecast/ec2-instance-fail.yaml
+++ /dev/null
@@ -1,76 +0,0 @@
-AWSTemplateFormatVersion: "2010-09-09"
-
-Description: |
-  AWS CloudFormation sample template.  
-  Create an Amazon EC2 instance with cfn-init and cfn-signal.
-
-Resources:
-  Instance:
-    Type: AWS::EC2::Instance
-    Metadata:
-      guard:
-        SuppressedRules:
-          - EC2_INSTANCES_IN_VPC
-      AWS::CloudFormation::Init:
-        config:
-          packages:
-            yum:
-              httpd: []
-          files:
-            /var/www/html/index.html:
-              content: |
-                <body>
-                  <h1>Congratulations, you have successfully launched the AWS CloudFormation sample.</h1>
-                </body>
-              mode: "000644"
-              owner: root
-              group: root
-            /etc/cfn/cfn-hup.conf:
-              content: !Sub |
-                [main]
-                stack=${AWS::StackId}
-                region=${AWS::Region}
-              mode: "000400"
-              owner: root
-              group: root
-            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
-              content: !Sub |-
-                [cfn-auto-reloader-hook]
-                triggers=post.update
-                path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init
-                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Instance --region ${AWS::Region}
-                runas=root
-          services:
-            sysvinit:
-              httpd:
-                enabled: true
-                ensureRunning: true
-              cfn-hup:
-                enabled: true
-                ensureRunning: true
-                files:
-                  - /etc/cfn/cfn-hup.conf
-                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
-    Properties:
-      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64}}'
-      InstanceType: t2.micro
-      KeyName: sample
-      BlockDeviceMappings:
-        - DeviceName: /dev/sda1
-          Ebs:
-            VolumeSize: 32
-      UserData: !Base64
-        Fn::Sub: |-
-          #!/bin/bash
-          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource WaitHandle --region ${AWS::Region}
-
-  WaitHandle:
-    Type: AWS::CloudFormation::WaitConditionHandle
-
-  WaitCondition:
-    Type: AWS::CloudFormation::WaitCondition
-    DependsOn: Instance
-    Properties:
-      Handle: !Ref WaitHandle
-      Timeout: 300
-      Count: 1
diff --git a/test/templates/forecast/ec2-instance-succeed.yaml b/test/templates/forecast/ec2-instance-succeed.yaml
deleted file mode 100644
index bdc24112..00000000
--- a/test/templates/forecast/ec2-instance-succeed.yaml
+++ /dev/null
@@ -1,69 +0,0 @@
-AWSTemplateFormatVersion: "2010-09-09"
-
-Description: |
-  AWS CloudFormation sample template.  
-  Create an Amazon EC2 instance with cfn-init and cfn-signal.
-
-Resources:
-  Instance:
-    Type: AWS::EC2::Instance
-    CreationPolicy:
-      ResourceSignal:
-        Timeout: PT5M
-    Metadata:
-      guard:
-        SuppressedRules:
-          - EC2_INSTANCES_IN_VPC
-      AWS::CloudFormation::Init:
-        config:
-          packages:
-            yum:
-              httpd: []
-          files:
-            /var/www/html/index.html:
-              content: |
-                <body>
-                  <h1>Congratulations, you have successfully launched the AWS CloudFormation sample.</h1>
-                </body>
-              mode: "000644"
-              owner: root
-              group: root
-            /etc/cfn/cfn-hup.conf:
-              content: !Sub |
-                [main]
-                stack=${AWS::StackId}
-                region=${AWS::Region}
-              mode: "000400"
-              owner: root
-              group: root
-            /etc/cfn/hooks.d/cfn-auto-reloader.conf:
-              content: !Sub |-
-                [cfn-auto-reloader-hook]
-                triggers=post.update
-                path=Resources.LaunchConfig.Metadata.AWS::CloudFormation::Init
-                action=/opt/aws/bin/cfn-init -v --stack ${AWS::StackName} --resource Instance --region ${AWS::Region}
-                runas=root
-          services:
-            sysvinit:
-              httpd:
-                enabled: true
-                ensureRunning: true
-              cfn-hup:
-                enabled: true
-                ensureRunning: true
-                files:
-                  - /etc/cfn/cfn-hup.conf
-                  - /etc/cfn/hooks.d/cfn-auto-reloader.conf
-    Properties:
-      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-6.1-arm64}}'
-      InstanceType: t4g.nano
-      KeyName: sample
-      BlockDeviceMappings:
-        - DeviceName: /dev/sda1
-          Ebs:
-            VolumeSize: 32
-      UserData: !Base64
-        Fn::Sub: |-
-          #!/bin/bash
-          /opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} --resource Instance --region ${AWS::Region}
-
diff --git a/test/templates/forecast/lambda-fail.yaml b/test/templates/forecast/lambda-fail.yaml
deleted file mode 100644
index 7c82a902..00000000
--- a/test/templates/forecast/lambda-fail.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-AWSTemplateFormatVersion: 2010-09-09
-
-Description: |
-  This template creates a lambda function that fails to deploy because it 
-  refers to a hard-coded arn that does not exist, and a second lambda function
-  that fails to deploy because it refers to a role lambda can't assume.
-  
-
-Resources:
-
-  LambdaFunction:
-    Type: AWS::Lambda::Function
-    Properties:
-      FunctionName: NonexistentRoleArn
-      Handler: index.handler
-      Role: "arn:aws:iam::123456789012:role/DoesNotExist"
-      Code:
-        ZipFile: |
-          exports.handler = async (event) => {
-            console.log(event);
-            return 'Hello from Lambda!';
-          };
-      Runtime: nodejs20.x
-      Timeout: 30
-      MemorySize: 128
-
-  LambdaFunction2:
-    Type: AWS::Lambda::Function
-    Properties:
-      FunctionName: InvalidRoleArn
-      Handler: index.handler
-      Role: "arn:aws:iam::755952356119:role/cep-alpha-awscommunity-CloudFrontLoggingEnabledBui-18841URX8LHC6"
-      Code:
-        ZipFile: |
-          exports.handler = async (event) => {
-            console.log(event);
-            return 'Hello from Lambda!';
-          };
-      Runtime: nodejs20.x
-      Timeout: 30
-      MemorySize: 128
diff --git a/test/templates/forecast/lambda-succeed.yaml b/test/templates/forecast/lambda-succeed.yaml
deleted file mode 100644
index 5984beb4..00000000
--- a/test/templates/forecast/lambda-succeed.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-AWSTemplateFormatVersion: 2010-09-09
-
-Description: |
-  This template creates a lambda function that successfully deploys
-
-Resources:
-
-  LambdaFunction:
-    Type: AWS::Lambda::Function
-    Properties:
-      FunctionName: Rain-Forecast-RoleIsValid
-      Handler: index.handler
-      Role: "arn:aws:iam::755952356119:role/lambda-basic"
-      Code:
-        ZipFile: |
-          exports.handler = async (event) => {
-            console.log(event);
-            return 'Hello from Lambda!';
-          };
-      Runtime: nodejs20.x
-      Timeout: 30
-      MemorySize: 128