Repository: aquasecurity/tfsec
Pull request #968 (2021-07-29 15:03:36)

Rule generation from CLI/CSV


Patch:
diff --git a/cmd/tfsec-skeleton/csv.go b/cmd/tfsec-skeleton/csv.go
new file mode 100644
index 000000000..ed3d033a1
--- /dev/null
+++ b/cmd/tfsec-skeleton/csv.go
@@ -0,0 +1,101 @@
+package main
+
+import (
+	"encoding/csv"
+	"fmt"
+	"os"
+	"strings"
+
+	"github.com/aquasecurity/tfsec/cmd/tfsec-skeleton/examples"
+	"github.com/aquasecurity/tfsec/cmd/tfsec-skeleton/requirements"
+	"github.com/aquasecurity/tfsec/pkg/provider"
+)
+
+func generateFromCSV(path string) ([]*Input, error) {
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	}
+	defer file.Close()
+
+	reader := csv.NewReader(file)
+
+	reader.FieldsPerRecord = 16
+
+	rawCSVdata, err := reader.ReadAll()
+	if err != nil {
+		return nil, err
+	}
+
+	var inputs []*Input
+
+	for i, record := range rawCSVdata {
+
+		// not ready
+		if record[0] != "1" {
+			continue
+		}
+
+		// already done
+		if record[1] == "1" {
+			continue
+		}
+
+		var input Input
+		input.Summary = record[2]
+		input.Provider = provider.Provider(record[3])
+		input.Service = record[4]
+		input.ShortCode = record[5]
+		input.RequiredTypes = []string{"resource"}
+		input.RequiredLabels = strings.Split(record[6], ",")
+
+		attrDotPath := record[7]
+		pathParts := strings.Split(attrDotPath, ".")
+		input.AttributeName = pathParts[len(pathParts)-1]
+		comparison := record[8]
+		value := convertToInterface(record[9])
+		allowDefault := record[10] == "1" || record[10] == "TRUE"
+
+		exampleCode := record[15]
+		if exampleCode == "" {
+			exampleCode, _ = examples.FindCode(string(input.Provider), input.RequiredTypes[0], input.RequiredLabels[0])
+		}
+
+		input.Severity = record[11]
+		input.Impact = record[12]
+		input.Resolution = record[13]
+		input.Explanation = record[14]
+
+		if comparison == "custom" {
+			input.Requirement = requirements.Custom(
+				input.RequiredTypes[0],
+				input.RequiredLabels[0],
+				exampleCode,
+			)
+		} else {
+			comp := requirements.Comparison(comparison)
+			if !comp.IsValid() {
+				return nil, fmt.Errorf("invalid comparison '%s' on line %d", comparison, i)
+			}
+
+			input.Requirement = requirements.NewAttributeRequirement(
+				input.RequiredTypes[0],
+				input.RequiredLabels[0],
+				attrDotPath,
+				value,
+				!allowDefault,
+				exampleCode,
+				comp,
+			)
+		}
+
+		if err := input.Validate(); err != nil {
+			return nil, fmt.Errorf("line %d of csv failed validation: %s", i, err)
+		}
+
+		inputs = append(inputs, &input)
+	}
+
+	return inputs, nil
+
+}
diff --git a/cmd/tfsec-skeleton/definition.go b/cmd/tfsec-skeleton/definition.go
new file mode 100644
index 000000000..ab4a87f0b
--- /dev/null
+++ b/cmd/tfsec-skeleton/definition.go
@@ -0,0 +1,69 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"text/template"
+
+	"github.com/aquasecurity/tfsec/cmd/tfsec-skeleton/requirements"
+)
+
+type Definition struct {
+	*Input
+	TestName        string
+	CheckFilename   string
+	TestFileName    string
+	Package         string
+	BadExampleCode  string
+	GoodExampleCode string
+	FirstLink       string
+	RuleCode        string
+	Requirements    []requirements.Requirement
+}
+
+func writeRuleFromInput(input *Input, forceOverwrite bool) error {
+	definition := Definition{
+		Input: input,
+	}
+
+	definition.TestName = fmt.Sprintf("%s%s", definition.Provider.ConstName(), strings.ReplaceAll(strings.Title(definition.ShortCode), "-", ""))
+	definition.Package = strings.ReplaceAll(definition.Service, "-", "")
+	definition.FirstLink = findDocLink(string(definition.Provider), definition.RequiredTypes[0], definition.RequiredLabels[0], input.AttributeName)
+
+	definition.BadExampleCode = definition.Requirement.GenerateBadExample()
+	definition.GoodExampleCode = definition.Requirement.GenerateGoodExample()
+	definition.RuleCode = definition.Requirement.GenerateRuleCode()
+
+	filename := fmt.Sprintf("%s/%s/%s", definition.Provider, definition.Package, strings.ReplaceAll(definition.ShortCode, "-", "_"))
+	definition.CheckFilename = fmt.Sprintf("%s_rule.go", strings.ToLower(filename))
+	definition.TestFileName = fmt.Sprintf("%s_rule_test.go", strings.ToLower(filename))
+
+	checkTmpl := template.Must(template.New("check").Parse(checkTemplate))
+	checkTestTmpl := template.Must(template.New("checkTest").Parse(checkTestTemplate))
+	checkPath := fmt.Sprintf("internal/app/tfsec/rules/%s", definition.CheckFilename)
+	testPath := fmt.Sprintf("internal/app/tfsec/rules/%s", definition.TestFileName)
+
+	if err := os.MkdirAll(filepath.Dir(checkPath), 0700); err != nil {
+		return err
+	}
+
+	if !forceOverwrite {
+		if err := verifyPathDoesNotExist(checkPath); err != nil {
+			return err
+		}
+		if err := verifyPathDoesNotExist(testPath); err != nil {
+			return err
+		}
+	}
+
+	if err := writeTemplate(checkPath, checkTmpl, definition); err != nil {
+		return err
+	}
+	if err := writeTemplate(testPath, checkTestTmpl, definition); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/cmd/tfsec-skeleton/examples/examples.go b/cmd/tfsec-skeleton/examples/examples.go
new file mode 100644
index 000000000..5062cfba4
--- /dev/null
+++ b/cmd/tfsec-skeleton/examples/examples.go
@@ -0,0 +1,85 @@
+package examples
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"strings"
+)
+
+func FindCode(provider string, blockType string, typeLabel string) (string, error) {
+
+	var shortCode string
+	switch blockType {
+	case "resource":
+		shortCode = "r"
+	case "data":
+		shortCode = "d"
+	default:
+		return "", fmt.Errorf("unsupported block type")
+	}
+
+	var urls []string
+
+	provider = strings.ToLower(provider)
+
+	switch provider {
+	case "azure":
+		provider = "azurerm"
+	}
+
+	urls = append(urls, fmt.Sprintf(
+		"https://raw.githubusercontent.com/hashicorp/terraform-provider-%s/master/website/docs/%s/%s.html.markdown",
+		provider,
+		shortCode,
+		typeLabel,
+	))
+	urls = append(urls, fmt.Sprintf(
+		"https://raw.githubusercontent.com/hashicorp/terraform-provider-%s/master/website/docs/%s/%s.html.markdown",
+		provider,
+		shortCode,
+		strings.TrimPrefix(typeLabel, provider+"_"),
+	))
+
+	var rawMarkdown string
+
+	for _, url := range urls {
+		if func(url string) bool {
+			resp, err := http.Get(url)
+			if err != nil {
+				return false
+			}
+			defer resp.Body.Close()
+			if resp.StatusCode >= 400 {
+				return false
+			}
+			code, err := ioutil.ReadAll(resp.Body)
+			if err != nil {
+				return false
+			}
+			rawMarkdown = string(code)
+			return true
+		}(url) {
+			break
+		}
+	}
+
+	if rawMarkdown == "" {
+		return "", fmt.Errorf("no docs found")
+	}
+
+	var example []string
+	var inExample bool
+	for _, line := range strings.Split(rawMarkdown, "\n") {
+		if strings.HasPrefix(line, "```hcl") {
+			inExample = true
+			continue
+		} else if strings.HasPrefix(line, "```") {
+			break
+		} else if inExample {
+			example = append(example, strings.TrimSuffix(line, "\r"))
+		}
+	}
+
+	return strings.Join(example, "\n"), nil
+}
diff --git a/cmd/tfsec-skeleton/examples/mutate.go b/cmd/tfsec-skeleton/examples/mutate.go
new file mode 100644
index 000000000..d5b39b861
--- /dev/null
+++ b/cmd/tfsec-skeleton/examples/mutate.go
@@ -0,0 +1,155 @@
+package examples
+
+import (
+	"fmt"
+	"strings"
+)
+
+type machine struct {
+	stack           []string
+	inPath          bool
+	found           bool
+	output          []string
+	tabStr          string
+	tabChecked      bool
+	multilineMarker string
+	dotPath         string
+	value           interface{}
+	renameResource  string
+}
+
+func (m *machine) calcTabs(line string) {
+	if !m.tabChecked && strings.TrimPrefix(line, " ") != line {
+		var size int
+		for _, c := range line {
+			if c == ' ' {
+				size++
+				continue
+			}
+			break
+		}
+		m.tabChecked = true
+		m.tabStr = strings.Repeat(" ", size)
+	}
+}
+
+func (m *machine) processAttributeLine(line string) string {
+	parts := strings.Split(line, "=")
+	name := strings.TrimSpace(parts[0])
+	if strings.Join(append(m.stack, name), ".") == m.dotPath {
+		line = fmt.Sprintf("%s= %s", parts[0], sprintGo(m.value))
+		m.found = true
+	}
+	if strings.Contains(line, "<<") {
+		m.multilineMarker = strings.Split(line, "<<")[1]
+	}
+	if strings.HasSuffix(line, "{") {
+		m.stack = append(m.stack, name)
+	}
+	return line
+}
+
+func (m *machine) processBlockOpening(line string) string {
+	if m.renameResource != "" && len(strings.Split(m.dotPath, ".")) >= 3 {
+		if dotSegmentFromLine(line) == strings.Join(strings.Split(m.dotPath, ".")[:3], ".") {
+			parts := strings.Split(strings.TrimSpace(line), " ")
+			line = fmt.Sprintf("%s %s \"%s\" {", parts[0], parts[1], m.renameResource)
+		}
+	}
+	m.stack = append(m.stack, dotSegmentFromLine(line))
+	return line
+}
+
+func (m *machine) processBlockClosing(line string) string {
+	if m.inPath && !m.found {
+		// we were in the right place - did we find our attr? if not, we need to add it
+		inject := expandPathAndValue(strings.TrimPrefix(m.dotPath, strings.Join(m.stack, ".")+"."), m.value, m.tabStr, len(m.stack))
+		m.output = append(m.output, inject)
+		m.found = true
+	}
+
+	m.stack = m.stack[:len(m.stack)-1]
+	return line
+}
+
+func (m *machine) processLine(line string) {
+
+	if m.multilineMarker != "" {
+		if line == m.multilineMarker {
+			m.multilineMarker = ""
+		}
+		m.output = append(m.output, line)
+		return
+	}
+
+	m.calcTabs(line)
+
+	if strings.Contains(line, "=") {
+		// handle attribute definition
+		line = m.processAttributeLine(line)
+	} else if strings.HasSuffix(strings.TrimSpace(line), "{") {
+		line = m.processBlockOpening(line)
+	} else if strings.HasSuffix(strings.TrimSpace(line), "}") {
+		line = m.processBlockClosing(line)
+	}
+
+	m.inPath = strings.HasPrefix(m.dotPath, strings.Join(m.stack, "."))
+	m.output = append(m.output, line)
+}
+
+/*
+ SetAttribute sets a given attribute value from a dot separated hcl path. resource paths should be prefixed with "resource."
+	This assumes a certain standard of clean Terraform code as is available in the provider examples.
+	It's used for building rule example code and thus only builds an approximation of what is needed, to be manually tweaked afterwards.
+*/
+func SetAttribute(rawHCL string, dotPath string, value interface{}, renameResource string) string {
+
+	m := machine{
+		tabStr:         "\t",
+		dotPath:        dotPath,
+		value:          value,
+		renameResource: renameResource,
+	}
+
+	for _, line := range strings.Split(rawHCL, "\n") {
+		m.processLine(line)
+	}
+
+	return strings.Join(m.output, "\n")
+}
+
+func expandPathAndValue(dotPath string, value interface{}, tabStr string, tabSize int) string {
+	tab := strings.Repeat(tabStr, tabSize)
+	segments := strings.Split(dotPath, ".")
+	if len(segments) == 1 {
+		return fmt.Sprintf("%s%s = %s", tab, segments[0], sprintGo(value))
+	}
+
+	return fmt.Sprintf("%s%s {\n%s\n%s}", tab, segments[0], expandPathAndValue(strings.Join(segments[1:], "."), value, tabStr, tabSize+1), tab)
+}
+
+func dotSegmentFromLine(line string) string {
+	parts := strings.Split(strings.Split(strings.TrimSpace(line), "{")[0], " ")
+	var output []string
+	for i, part := range parts {
+		if i == 2 {
+			part = "*"
+		}
+		part = strings.ReplaceAll(part, `"`, "")
+		if part == "" {
+			continue
+		}
+		output = append(output, part)
+	}
+	return strings.Join(output, ".")
+}
+
+func sprintGo(value interface{}) string {
+	if s, ok := value.(string); ok {
+		return fmt.Sprintf("%q", s)
+	}
+	if s, ok := value.([]string); ok {
+		return fmt.Sprintf("%#v", s)
+	}
+	return fmt.Sprintf("%v", value)
+}
diff --git a/cmd/tfsec-skeleton/generate.go b/cmd/tfsec-skeleton/generate.go
deleted file mode 100644
index b8f087f92..000000000
--- a/cmd/tfsec-skeleton/generate.go
+++ /dev/null
@@ -1,134 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"path/filepath"
-	"sort"
-	"strings"
-	"text/template"
-
-	"github.com/liamg/clinch/prompt"
-)
-
-var providers = map[string]string{"AWS": "aws", "Azure": "azu", "Google": "gcp", "Oracle": "oci", "General": "gen", "DigitalOcean": "dig", "GitHub": "git", "OpenStack": "ost"}
-
-type checkSkeleton struct {
-	Provider         string
-	ProviderLongName string
-	CheckName        string
-	ShortCode        string
-	FullCode         string
-	Service          string
-	Summary          string
-	Impact           string
-	Resolution       string
-	RequiredTypes    string
-	RequiredLabels   string
-	CheckFilename    string
-	TestFileName     string
-	Package          string
-}
-
-var funcMap = template.FuncMap{
-	"ToUpper": strings.ToUpper,
-}
-
-func generateCheckBody() error {
-	details, err := constructSkeleton()
-	if err != nil {
-		return err
-	}
-	checkTmpl := template.Must(template.New("check").Funcs(funcMap).Parse(checkTemplate))
-	checkTestTmpl := template.Must(template.New("checkTest").Funcs(funcMap).Parse(checkTestTemplate))
-	checkPath := fmt.Sprintf("internal/app/tfsec/rules/%s", details.CheckFilename)
-	testPath := fmt.Sprintf("internal/app/tfsec/rules/%s", details.TestFileName)
-
-	_ = os.Mkdir(filepath.Dir(checkPath), 0700)
-
-	if err = verifyCheckPath(checkPath); err != nil {
-		return err
-	}
-	if err = verifyCheckPath(testPath); err != nil {
-		return err
-	}
-	if err = writeTemplate(checkPath, checkTmpl, details); err != nil {
-		return err
-	}
-	if err = writeTemplate(testPath, checkTestTmpl, details); err != nil {
-		return err
-	}
-	return nil
-}
-
-func writeTemplate(checkPath string, checkTmpl *template.Template, details *checkSkeleton) error {
-	checkFile, err := os.Create(checkPath)
-	if err != nil {
-		return err
-	}
-	defer func() { _ = checkFile.Close() }()
-	err = checkTmpl.Execute(checkFile, details)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-func constructSkeleton() (*checkSkeleton, error) {
-	var providerStrings []string
-	for key := range providers {
-		providerStrings = append(providerStrings, key)
-	}
-
-	sort.Slice(providerStrings, func(i, j int) bool {
-		return providerStrings[i] < providerStrings[j]
-	})
-
-	_, selected, err := prompt.ChooseFromList("Select provider", providerStrings)
-	if err != nil {
-		return nil, err
-	}
-
-	service := prompt.EnterInput("Enter the service name, as see in terraform e.g. 'compute' for azurerm: ")
-	shortCode := prompt.EnterInput("Enter very terse description e.g. 'enable disk encryption': ")
-	summary := prompt.EnterInput("Enter a longer summary: ")
-	impact := prompt.EnterInput("Enter a brief impact of not complying with check: ")
-	resolution := prompt.EnterInput("Enter a brief resolution to pass check: ")
-	blockTypes := prompt.EnterInput("Enter the supported block types: ")
-	blockLabels := prompt.EnterInput("Enter the supported block labels: ")
-
-	checkBody, err := populateSkeleton(summary, selected, service, shortCode, impact, resolution, blockTypes, blockLabels)
-	if err != nil {
-		return nil, err
-	}
-
-	return checkBody, nil
-}
-
-func populateSkeleton(summary, selected, service, shortCode, impact, resolution, blockTypes, blockLabels string) (*checkSkeleton, error) {
-	checkBody := &checkSkeleton{}
-	var err error
-	checkBody.Summary = summary
-	checkBody.ShortCode = shortCode
-	checkBody.FullCode = strings.ToLower(fmt.Sprintf("%s-%s-%s", selected, service, shortCode))
-	checkBody.Service = service
-	checkBody.Impact = impact
-	checkBody.Resolution = resolution
-	checkBody.Provider = providers[selected]
-	checkBody.ProviderLongName = selected
-	checkBody.Package = strings.ReplaceAll(service, "-", "")
-
-	if err != nil {
-		return nil, err
-	}
-
-	checkBody.CheckName = fmt.Sprintf("%s%s", strings.Title(selected), strings.ReplaceAll(strings.Title(shortCode), "-", ""))
-	checkBody.RequiredTypes = fmt.Sprintf("{\"%s\"}", strings.Join(strings.Split(blockTypes, " "), "\", \""))
-	checkBody.RequiredLabels = fmt.Sprintf("{\"%s\"}", strings.Join(strings.Split(blockLabels, " "), "\", \""))
-	filename := fmt.Sprintf("%s/%s/%s", selected, checkBody.Package, strings.ReplaceAll(shortCode, "-", "_"))
-	checkBody.CheckFilename = fmt.Sprintf("%s_rule.go", strings.ToLower(filename))
-	checkBody.TestFileName = fmt.Sprintf("%s_rule_test.go", strings.ToLower(filename))
-
-	return checkBody, nil
-}
diff --git a/cmd/tfsec-skeleton/generation.go b/cmd/tfsec-skeleton/generation.go
new file mode 100644
index 000000000..b63f63d9a
--- /dev/null
+++ b/cmd/tfsec-skeleton/generation.go
@@ -0,0 +1,126 @@
+package main
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"strconv"
+	"strings"
+	"text/template"
+
+	"github.com/liamg/clinch/prompt"
+)
+
+var providers = []string{
+	"AWS",
+	"Azure",
+	"DigitalOcean",
+	"General",
+	"GitHub",
+	"Google",
+	"OpenStack",
+	"Oracle",
+}
+
+func writeTemplate(checkPath string, checkTmpl *template.Template, definition Definition) error {
+	checkFile, err := os.Create(checkPath)
+	if err != nil {
+		return err
+	}
+	defer func() { _ = checkFile.Close() }()
+	err = checkTmpl.Execute(checkFile, definition)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func verifyPathDoesNotExist(checkPath string) error {
+	stat, _ := os.Stat(checkPath)
+	if stat != nil {
+		return fmt.Errorf("file [%s] already exists so not creating check", checkPath)
+	}
+	return nil
+}
+
+func inputYesNo(msg string) bool {
+	failForDefaultStr := prompt.EnterInputWithDefault(fmt.Sprintf("%s (Y/n)", msg), "y")
+	return strings.ToLower(failForDefaultStr[0:1]) == "y"
+}
+
+func enterInterfaceValue(msg string) interface{} {
+	raw := prompt.EnterInput(msg)
+
+	return convertToInterface(raw)
+}
+
+func convertToInterface(raw string) interface{} {
+
+	if strings.Contains(raw, ",") {
+		return strings.Split(raw, ",")
+	}
+
+	lower := strings.ToLower(raw)
+	if lower == "true" || lower == "false" {
+		return lower == "true"
+	}
+
+	if i, err := strconv.Atoi(raw); err == nil {
+		return i
+	}
+
+	return raw
+}
+
+func findDocLink(provider string, blockType string, blockLabel string, attribute string) string {
+	provider = strings.ToLower(provider)
+	typeStr := "resources"
+	shortCode := "r"
+	if blockType == "data" {
+		typeStr = "data-sources"
+		shortCode = "d"
+	}
+
+	if urlIsGood(fmt.Sprintf(
+		"https://raw.githubusercontent.com/hashicorp/terraform-provider-%s/master/website/docs/%s/%s.html.markdown",
+		provider,
+		shortCode,
+		blockLabel,
+	)) {
+		return fmt.Sprintf(
+			"https://registry.terraform.io/providers/hashicorp/%s/latest/docs/%s/%s#%s",
+			provider,
+			typeStr,
+			blockLabel,
+			attribute,
+		)
+	}
+
+	shortBlock := strings.TrimPrefix(blockLabel, provider+"_")
+	if urlIsGood(fmt.Sprintf(
+		"https://raw.githubusercontent.com/hashicorp/terraform-provider-%s/master/website/docs/%s/%s.html.markdown",
+		provider,
+		shortCode,
+		shortBlock,
+	)) {
+		return fmt.Sprintf(
+			"https://registry.terraform.io/providers/hashicorp/%s/latest/docs/%s/%s#%s",
+			provider,
+			typeStr,
+			shortBlock,
+			attribute,
+		)
+	}
+
+	return ""
+}
+
+func urlIsGood(url string) bool {
+	resp, err := http.Get(url)
+	if err != nil {
+		return false
+	}
+	defer resp.Body.Close()
+	return resp.StatusCode < 400
+}
diff --git a/cmd/tfsec-skeleton/input.go b/cmd/tfsec-skeleton/input.go
new file mode 100644
index 000000000..35fc3aa11
--- /dev/null
+++ b/cmd/tfsec-skeleton/input.go
@@ -0,0 +1,63 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/aquasecurity/tfsec/cmd/tfsec-skeleton/requirements"
+	"github.com/aquasecurity/tfsec/pkg/provider"
+)
+
+type Input struct {
+	Provider       provider.Provider
+	Service        string
+	ShortCode      string
+	Summary        string
+	Explanation    string
+	Impact         string
+	Resolution     string
+	Severity       string
+	RequiredTypes  []string
+	RequiredLabels []string
+	Requirement    requirements.Requirement
+	AttributeName  string
+}
+
+func (i Input) Validate() error {
+	if i.Provider == "" {
+		return fmt.Errorf("no provider specified")
+	}
+	if i.Service == "" {
+		return fmt.Errorf("no service specified")
+	}
+	if i.ShortCode == "" {
+		return fmt.Errorf("no short code specified")
+	}
+	if i.Summary == "" {
+		return fmt.Errorf("no summary specified")
+	}
+	if i.Explanation == "" {
+		return fmt.Errorf("no explanation specified")
+	}
+	if i.Impact == "" {
+		return fmt.Errorf("no impact specified")
+	}
+	if i.Resolution == "" {
+		return fmt.Errorf("no resolution specified")
+	}
+	if i.Severity == "" {
+		return fmt.Errorf("no severity specified")
+	}
+	if len(i.RequiredTypes) == 0 {
+		return fmt.Errorf("no required type(s) specified")
+	}
+	if len(i.RequiredLabels) == 0 {
+		return fmt.Errorf("no required label(s) specified")
+	}
+	if i.Requirement == nil {
+		return fmt.Errorf("no requirement specified")
+	}
+	if i.AttributeName == "" {
+		return fmt.Errorf("no attribute path specified")
+	}
+	return nil
+}
diff --git a/cmd/tfsec-skeleton/interactive.go b/cmd/tfsec-skeleton/interactive.go
new file mode 100644
index 000000000..d9d0059f2
--- /dev/null
+++ b/cmd/tfsec-skeleton/interactive.go
@@ -0,0 +1,178 @@
+package main
+
+import (
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path"
+	"strings"
+
+	"github.com/aquasecurity/tfsec/cmd/tfsec-skeleton/examples"
+	"github.com/aquasecurity/tfsec/cmd/tfsec-skeleton/requirements"
+	"github.com/aquasecurity/tfsec/pkg/provider"
+	"github.com/liamg/clinch/prompt"
+)
+
+func (input *Input) trySwap() error {
+	if swapExists() {
+		if inputYesNo("Swap file found! Would you like to continue your progress?") {
+			loaded, err := loadFromSwap()
+			if err != nil {
+				return err
+			}
+			*input = *loaded
+			fmt.Println("Swap file loaded!")
+		}
+	}
+	return nil
+}
+
+func (input *Input) readInitialInputs() error {
+
+	if input.Provider == "" {
+		_, providerStr, err := prompt.ChooseFromList("Select provider:", providers)
+		if err != nil {
+			return err
+		}
+		input.Provider = provider.Provider(providerStr)
+		saveSwapFile(input)
+	}
+
+	if input.Service == "" {
+		input.Service = prompt.EnterInput("Enter the service name, as seen in terraform e.g. 'compute' for azurerm: ")
+		saveSwapFile(input)
+	}
+
+	if input.ShortCode == "" {
+		input.ShortCode = prompt.EnterInput("Enter very terse description e.g. 'enable disk encryption': ")
+		saveSwapFile(input)
+	}
+
+	if input.Summary == "" {
+		input.Summary = prompt.EnterInput("Enter a longer summary: ")
+		saveSwapFile(input)
+	}
+
+	if input.Explanation == "" {
+		input.Explanation = prompt.EnterInput("Enter an explanation: ")
+		saveSwapFile(input)
+	}
+
+	if input.Impact == "" {
+		input.Impact = prompt.EnterInput("Enter a brief impact of not complying with check: ")
+		saveSwapFile(input)
+	}
+
+	if input.Resolution == "" {
+		input.Resolution = prompt.EnterInput("Enter a brief resolution to pass check: ")
+		saveSwapFile(input)
+	}
+
+	if input.Severity == "" {
+		_, severityStr, err := prompt.ChooseFromList("Which severity should be used?", []string{"Critical", "High", "Medium", "Low"})
+		if err != nil {
+			return err
+		}
+		input.Severity = severityStr
+		saveSwapFile(input)
+	}
+
+	if len(input.RequiredTypes) == 0 {
+		blockTypes := prompt.EnterInput("Enter the supported block types (e.g. resource): ")
+		input.RequiredTypes = strings.Split(blockTypes, ",")
+		saveSwapFile(input)
+	}
+	if len(input.RequiredLabels) == 0 {
+		blockLabels := prompt.EnterInput("Enter the supported block labels (e.g. aws_s3_bucket: ")
+		input.RequiredLabels = strings.Split(blockLabels, ",")
+		saveSwapFile(input)
+	}
+
+	return nil
+}
+
+func (input *Input) gatherInputsInteractively() error {
+
+	if err := input.trySwap(); err != nil {
+		return err
+	}
+
+	if err := input.readInitialInputs(); err != nil {
+		return err
+	}
+
+	requirementTypes := []requirements.Comparison{
+		requirements.ComparisonEquals,
+		requirements.ComparisonNotEquals,
+		requirements.ComparisonAnyOf,
+		requirements.ComparisonNotAnyOf,
+		requirements.ComparisonGreaterThan,
+		requirements.ComparisonLessThan,
+		requirements.ComparisonGreaterThanOrEqual,
+		requirements.ComparisonLessThanOrEqual,
+	}
+
+	var requirementStrs []string
+	for _, comparison := range requirementTypes {
+		requirementStrs = append(requirementStrs, string(comparison))
+	}
+	requirementStrs = append(requirementStrs, "custom")
+
+	requirementIndex, _, err := prompt.ChooseFromList("Which comparison does your rule involve?", requirementStrs)
+	if err != nil {
+		return err
+	}
+
+	exampleCode, _ := examples.FindCode(string(input.Provider), input.RequiredTypes[0], input.RequiredLabels[0])
+
+	if requirementIndex == len(requirementStrs)-1 {
+		input.Requirement = requirements.Custom(input.RequiredTypes[0], input.RequiredLabels[0], exampleCode)
+	} else {
+		dotPath := prompt.EnterInput("Enter the path to the attribute to check (e.g. settings.encryption.enabled): ")
+		expectedValue := enterInterfaceValue("Enter the value the attribute must have to pass the check (e.g. true): ")
+		failForDefault := inputYesNo("Should the check fail if a default value is used?")
+		input.Requirement = requirements.NewAttributeRequirement(input.RequiredTypes[0], input.RequiredLabels[0], dotPath, expectedValue, failForDefault, exampleCode, requirementTypes[requirementIndex])
+	}
+
+	_ = deleteSwap()
+
+	if err := input.Validate(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func swapPath() string {
+	return path.Join(os.TempDir(), "tfsec.swp")
+}
+
+func deleteSwap() error {
+	return os.Remove(swapPath())
+}
+
+func swapExists() bool {
+	_, err := os.Stat(swapPath())
+	return err == nil || !os.IsNotExist(err)
+}
+
+func loadFromSwap() (*Input, error) {
+	data, err := ioutil.ReadFile(swapPath())
+	if err != nil {
+		return nil, err
+	}
+	var input Input
+	if err := json.Unmarshal(data, &input); err != nil {
+		return nil, err
+	}
+	return &input, nil
+}
+
+func saveSwapFile(input *Input) error {
+	data, err := json.Marshal(input)
+	if err != nil {
+		return err
+	}
+	return ioutil.WriteFile(swapPath(), data, 0600)
+}
diff --git a/cmd/tfsec-skeleton/main.go b/cmd/tfsec-skeleton/main.go
index 61ab5dd4b..efebb13ac 100644
--- a/cmd/tfsec-skeleton/main.go
+++ b/cmd/tfsec-skeleton/main.go
@@ -4,10 +4,14 @@ import (
 	"fmt"
 	"os"
 
+	"github.com/liamg/clinch/terminal"
 	"github.com/spf13/cobra"
 )
 
+var forceOverwrite bool
+
 func main() {
+	rootCmd.Flags().BoolVarP(&forceOverwrite, "force-overwrite", "f", forceOverwrite, "Overwrite existing checks")
 	if err := rootCmd.Execute(); err != nil {
 		_, _ = fmt.Fprint(os.Stderr, err)
 		os.Exit(1)
@@ -19,7 +23,35 @@ var rootCmd = &cobra.Command{
 	Short: "tfsec-skeleton is a tfsec tool for generating code files for checks.",
 	Long: `tfsec-skeleton is a simple tool for generating check code files at a corresponding test file.
 `,
-	RunE: func(_ *cobra.Command, _ []string) error {
-		return generateCheckBody()
+	Args: cobra.MaximumNArgs(1),
+	RunE: func(_ *cobra.Command, args []string) error {
+
+		var inputs []*Input
+
+		if len(args) == 1 {
+			var err error
+			inputs, err = generateFromCSV(args[0])
+			if err != nil {
+				return err
+			}
+
+		} else {
+			input := &Input{}
+			if err := input.gatherInputsInteractively(); err != nil {
+				return err
+			}
+			inputs = []*Input{input}
+		}
+
+		for _, input := range inputs {
+			deref := *input
+			if err := writeRuleFromInput(&deref, forceOverwrite); err != nil {
+				return err
+			}
+			fmt.Printf("Added %s-%s-%s...\n", input.Provider, input.Service, input.ShortCode)
+		}
+
+		terminal.PrintSuccessf("\nAdded %d new rule(s).\n", len(inputs))
+		return nil
 	},
 }
diff --git a/cmd/tfsec-skeleton/requirements/attributes.go b/cmd/tfsec-skeleton/requirements/attributes.go
new file mode 100644
index 000000000..54408a57a
--- /dev/null
+++ b/cmd/tfsec-skeleton/requirements/attributes.go
@@ -0,0 +1,201 @@
+package requirements
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/aquasecurity/tfsec/cmd/tfsec-skeleton/examples"
+)
+
+func AttributeMustHaveValue(blockType string, blockLabel string, dotPath string, expectedValue interface{}, failCheckForDefaultValue bool, exampleCode string) Requirement {
+	return NewAttributeRequirement(blockType, blockLabel, dotPath, expectedValue, failCheckForDefaultValue, exampleCode, ComparisonEquals)
+}
+
+func AttributeMustNotHaveValue(blockType string, blockLabel string, dotPath string, expectedValue interface{}, failCheckForDefaultValue bool, exampleCode string) Requirement {
+	return NewAttributeRequirement(blockType, blockLabel, dotPath, expectedValue, failCheckForDefaultValue, exampleCode, ComparisonNotEquals)
+}
+
+type attributeBase struct {
+	dotPath                  string
+	value                    interface{}
+	comparison               Comparison
+	failCheckForDefaultValue bool
+	blockType                string
+	blockLabel               string
+	exampleCode              string
+}
+
+func NewAttributeRequirement(blockType string, blockLabel string, dotPath string, value interface{}, failCheckForDefaultValue bool, exampleCode string, comparison Comparison) Requirement {
+	var req attributeBase
+	req.dotPath = dotPath
+	req.value = value
+	req.comparison = comparison
+	req.failCheckForDefaultValue = failCheckForDefaultValue
+	req.blockType = blockType
+	req.blockLabel = blockLabel
+	req.exampleCode = exampleCode
+	return &req
+}
+
+func (a *attributeBase) makeGoodValue(value interface{}, comparison Comparison) interface{} {
+	switch a.comparison {
+	case ComparisonEquals:
+		return a.value
+	case ComparisonNotEquals:
+		return flipValue(a.value)
+	case ComparisonAnyOf:
+		if strs, ok := a.value.([]string); ok && len(strs) > 0 {
+			return strs[0]
+		}
+		panic("only non-zero length list of strings are supported")
+	case ComparisonNotAnyOf:
+		return "something"
+	case ComparisonGreaterThan:
+		if i, ok := a.value.(int); ok {
+			return i + 1
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	case ComparisonLessThan:
+		if i, ok := a.value.(int); ok {
+			return i - 1
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	case ComparisonGreaterThanOrEqual:
+		if i, ok := a.value.(int); ok {
+			return i
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	case ComparisonLessThanOrEqual:
+		if i, ok := a.value.(int); ok {
+			return i
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	default:
+		panic(fmt.Sprintf("comparison '%s' is not supported", a.comparison))
+	}
+}
+
+func (a *attributeBase) GenerateGoodExample() string {
+
+	value := a.makeGoodValue(a.value, a.comparison)
+
+	if a.exampleCode != "" {
+		return examples.SetAttribute(a.exampleCode, fmt.Sprintf("%s.%s.*.%s", a.blockType, a.blockLabel, a.dotPath), value, "good_example")
+	}
+
+	return fmt.Sprintf(`
+%s "%s" "%s" {
+%s}
+`, a.blockType, a.blockLabel, "good_example", createTerraformFromDotPath(a.dotPath, value))
+}
+
+func (a *attributeBase) makeBadValue(value interface{}, comparison Comparison) interface{} {
+	switch a.comparison {
+	case ComparisonEquals:
+		return flipValue(a.value)
+	case ComparisonNotEquals:
+		return a.value
+	case ComparisonAnyOf:
+		return "something"
+	case ComparisonNotAnyOf:
+		if strs, ok := a.value.([]string); ok && len(strs) > 0 {
+			return strs[0]
+		}
+		panic("only non-zero length list of strings are supported")
+	case ComparisonGreaterThan:
+		if i, ok := a.value.(int); ok {
+			return i - 1
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	case ComparisonLessThan:
+		if i, ok := a.value.(int); ok {
+			return i + 1
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	case ComparisonGreaterThanOrEqual:
+		if i, ok := a.value.(int); ok {
+			return i - 1
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	case ComparisonLessThanOrEqual:
+		if i, ok := a.value.(int); ok {
+			return i + 1
+		}
+		panic(fmt.Sprintf("comparison '%s' cannot support value %#v", a.comparison, a.value))
+	default:
+		panic(fmt.Sprintf("comparison'%s' is not supported", a.comparison))
+	}
+}
+
+func (a *attributeBase) GenerateBadExample() string {
+
+	value := a.makeBadValue(a.value, a.comparison)
+
+	if a.exampleCode != "" {
+		return examples.SetAttribute(a.exampleCode, fmt.Sprintf("%s.%s.*.%s", a.blockType, a.blockLabel, a.dotPath), value, "bad_example")
+	}
+
+	return fmt.Sprintf(`
+%s "%s" "%s" {
+%s}
+`, a.blockType, a.blockLabel, "bad_example", createTerraformFromDotPath(a.dotPath, value))
+}
+
+func (a *attributeBase) GenerateRuleCode() string {
+	var code string
+
+	lowestBlock := "resourceBlock"
+	parts := strings.Split(a.dotPath, ".")
+
+	getBlockCode := lowestBlock
+
+	for i, part := range parts {
+		if i == len(parts)-1 {
+			break
+		}
+		getBlockCode = fmt.Sprintf(`%s.GetBlock("%s")`, getBlockCode, part)
+	}
+
+	attrName := parts[len(parts)-1]
+	attrVarName := snakeToCamel(attrName) + "Attr"
+
+	code += fmt.Sprintf(`if %s := %s.GetAttribute("%s"); `, attrVarName, getBlockCode, attrName)
+
+	if a.failCheckForDefaultValue {
+		code += fmt.Sprintf(`%s.IsNil() { // alert on use of default value
+				set.AddResult().
+					WithDescription("Resource '%%s' uses default value for %s", resourceBlock.FullName())
+			} else if `, attrVarName, a.dotPath)
+	}
+
+	var messageTemplate string
+
+	switch a.comparison {
+	case ComparisonEquals:
+		messageTemplate = fmt.Sprintf("Resource '%%s' does not have %s set to %v", a.dotPath, a.value)
+	case ComparisonNotEquals:
+		messageTemplate = fmt.Sprintf("Resource '%%s' has %s set to %v", a.dotPath, a.value)
+	case ComparisonAnyOf:
+		messageTemplate = fmt.Sprintf("Resource '%%s' does not have %s set to one of %s", a.dotPath, a.value)
+	case ComparisonNotAnyOf:
+		messageTemplate = fmt.Sprintf("Resource '%%s' has %s set to one of %s", a.dotPath, a.value)
+	case ComparisonGreaterThan:
+		messageTemplate = fmt.Sprintf("Resource '%%s' does not have %s set to greater than %d", a.dotPath, a.value)
+	case ComparisonLessThan:
+		messageTemplate = fmt.Sprintf("Resource '%%s' does not have %s set to less than %d", a.dotPath, a.value)
+	case ComparisonGreaterThanOrEqual:
+		messageTemplate = fmt.Sprintf("Resource '%%s' does not have %s set to greater than or equal to %d", a.dotPath, a.value)
+	case ComparisonLessThanOrEqual:
+		messageTemplate = fmt.Sprintf("Resource '%%s' does not have %s set to greater than or equal to %d", a.dotPath, a.value)
+	default:
+		panic(fmt.Sprintf("comparison '%s' is not supported", a.comparison))
+	}
+
+	code += fmt.Sprintf(`%s.%s {
+				set.AddResult().
+					WithDescription("%s", resourceBlock.FullName()).
+					WithAttribute(%s)
+			}`, attrVarName, buildComparisonForValue(a.value, a.comparison.Reverse()), messageTemplate, attrVarName)
+
+	return code
+}
diff --git a/cmd/tfsec-skeleton/requirements/comparison.go b/cmd/tfsec-skeleton/requirements/comparison.go
new file mode 100644
index 000000000..ebe9cbef8
--- /dev/null
+++ b/cmd/tfsec-skeleton/requirements/comparison.go
@@ -0,0 +1,42 @@
+package requirements
+
+type Comparison string
+
+const (
+	ComparisonNone               Comparison = ""
+	ComparisonEquals             Comparison = "equals"
+	ComparisonNotEquals          Comparison = "notequals"
+	ComparisonAnyOf              Comparison = "anyof"
+	ComparisonNotAnyOf           Comparison = "notanyof"
+	ComparisonGreaterThan        Comparison = "greaterthan"
+	ComparisonLessThan           Comparison = "lessthan"
+	ComparisonGreaterThanOrEqual Comparison = "greaterthanorequal"
+	ComparisonLessThanOrEqual    Comparison = "lessthanorequal"
+)
+
+func (c Comparison) Reverse() Comparison {
+	switch c {
+	case ComparisonEquals:
+		return ComparisonNotEquals
+	case ComparisonNotEquals:
+		return ComparisonEquals
+	case ComparisonAnyOf:
+		return ComparisonNotAnyOf
+	case ComparisonNotAnyOf:
+		return ComparisonAnyOf
+	case ComparisonGreaterThan:
+		return ComparisonLessThanOrEqual
+	case ComparisonLessThan:
+		return ComparisonGreaterThanOrEqual
+	case ComparisonGreaterThanOrEqual:
+		return ComparisonLessThan
+	case ComparisonLessThanOrEqual:
+		return ComparisonGreaterThan
+	default:
+		return ComparisonNone
+	}
+}
+
+func (c Comparison) IsValid() bool {
+	return c.Reverse() != ComparisonNone
+}
diff --git a/cmd/tfsec-skeleton/requirements/custom.go b/cmd/tfsec-skeleton/requirements/custom.go
new file mode 100644
index 000000000..9122afd2e
--- /dev/null
+++ b/cmd/tfsec-skeleton/requirements/custom.go
@@ -0,0 +1,45 @@
+package requirements
+
+import (
+	"fmt"
+)
+
+type custom struct {
+	blockType   string
+	blockLabel  string
+	exampleCode string
+}
+
+func Custom(blockType string, blockLabel string, exampleCode string) Requirement {
+	var req custom
+	req.blockType = blockType
+	req.blockLabel = blockLabel
+	req.exampleCode = exampleCode
+	return &req
+}
+
+func (a *custom) GenerateGoodExample() string {
+	if a.exampleCode != "" {
+		return a.exampleCode
+	}
+	return fmt.Sprintf(`
+%s "%s" "%s" {
+
+}
+`, a.blockType, a.blockLabel, "good_example")
+}
+
+func (a *custom) GenerateBadExample() string {
+	if a.exampleCode != "" {
+		return a.exampleCode
+	}
+	return fmt.Sprintf(`
+%s "%s" "%s" {
+
+}
+`, a.blockType, a.blockLabel, "bad_example")
+}
+
+func (a *custom) GenerateRuleCode() string {
+	return " // TODO: code goes here"
+}
diff --git a/cmd/tfsec-skeleton/requirements/requirements.go b/cmd/tfsec-skeleton/requirements/requirements.go
new file mode 100644
index 000000000..5dd47d2cd
--- /dev/null
+++ b/cmd/tfsec-skeleton/requirements/requirements.go
@@ -0,0 +1,167 @@
+package requirements
+
+import (
+	"fmt"
+	"strings"
+)
+
+type Requirement interface {
+	GenerateGoodExample() string
+	GenerateBadExample() string
+	GenerateRuleCode() string
+}
+
+func sprintGo(value interface{}) string {
+	if s, ok := value.(string); ok {
+		return fmt.Sprintf("%q", s)
+	}
+	if s, ok := value.([]string); ok {
+		return fmt.Sprintf("%#v", s)
+	}
+	return fmt.Sprintf("%v", value)
+}
+
+func createTerraformFromDotPath(dotPath string, value interface{}) string {
+
+	var output string
+
+	parts := strings.Split(dotPath, ".")
+
+	for i, part := range parts {
+
+		if i == len(parts)-1 {
+			// attribute
+			output += fmt.Sprintf("%s%s = %s\n", strings.Repeat("\t", i+1), part, sprintGo(value))
+			break
+		}
+
+		// open block
+		output += fmt.Sprintf("%s%s {\n", strings.Repeat("\t", i+1), part)
+	}
+
+	// close all blocks
+	for i := len(parts) - 2; i >= 0; i-- {
+		output += fmt.Sprintf("%s}\n", strings.Repeat("\t", i+1))
+	}
+
+	return output
+}
+
+func flipValue(value interface{}) interface{} {
+	var badValue interface{}
+
+	switch t := value.(type) {
+	case bool:
+		badValue = !t
+	case string:
+		switch t {
+		case "on":
+			badValue = "off"
+		case "off":
+			badValue = "on"
+		case "yes":
+			badValue = "no"
+		case "no":
+			badValue = "yes"
+		default:
+			badValue = "something"
+		}
+	case int:
+		switch t {
+		case 0:
+			badValue = 1
+		case 1:
+			badValue = 0
+		default:
+			badValue = t + 1
+		}
+	}
+
+	return badValue
+}
+
+func snakeToCamel(input string) string {
+	var output string
+	var upper bool
+	for i, v := range input {
+		if v == '_' {
+			upper = true
+			continue
+		}
+		if upper {
+			output += strings.ToUpper(input[i : i+1])
+			upper = false
+			continue
+		}
+		output += strings.ToLower(input[i : i+1])
+	}
+	return output
+}
+
+func buildComparisonForValue(value interface{}, comparison Comparison) string {
+
+	switch t := value.(type) {
+	case []string:
+		return buildStringSliceComparison(t, comparison)
+	case int:
+		return buildIntComparison(t, comparison)
+	case string:
+		return buildStringComparison(t, comparison)
+	case bool:
+		return buildBoolComparison(t, comparison)
+	default:
+		panic(fmt.Sprintf("Cannot do comparisons on type %T", t))
+	}
+}
+
+func buildStringSliceComparison(values []string, comparison Comparison) string {
+	switch comparison {
+	case ComparisonAnyOf:
+		return fmt.Sprintf(`IsAny(%s)`, sprintGo(values))
+	case ComparisonNotAnyOf:
+		return fmt.Sprintf(`IsNone(%s)`, sprintGo(values))
+	}
+	panic(fmt.Sprintf("Comparison '%s' not supported for string slice", comparison))
+}
+
+func buildStringComparison(value string, comparison Comparison) string {
+	switch comparison {
+	case ComparisonEquals:
+		if value == "" {
+			return "IsEmpty()"
+		}
+		return fmt.Sprintf(`Equals(%s)`, sprintGo(value))
+	case ComparisonNotEquals:
+		if value == "" {
+			return "!IsEmpty()"
+		}
+		return fmt.Sprintf(`NotEqual(%s)`, sprintGo(value))
+	}
+	panic(fmt.Sprintf("Comparison '%s' not supported for string", comparison))
+}
+
+func buildIntComparison(value int, comparison Comparison) string {
+	switch comparison {
+	case ComparisonEquals:
+		return fmt.Sprintf(`Equals(%s)`, sprintGo(value))
+	case ComparisonNotEquals:
+		return fmt.Sprintf(`NotEqual(%s)`, sprintGo(value))
+	}
+	panic(fmt.Sprintf("Comparison '%s' not supported for int", comparison))
+}
+
+func buildBoolComparison(value bool, comparison Comparison) string {
+	switch comparison {
+	case ComparisonEquals:
+		if value {
+			return "IsTrue()"
+		}
+		return "IsFalse()"
+	case ComparisonNotEquals:
+		if value {
+			return "IsFalse()"
+		}
+		return "IsTrue()"
+	}
+	panic(fmt.Sprintf("Comparison '%s' not supported for bool", comparison))
+}
diff --git a/cmd/tfsec-skeleton/templates.go b/cmd/tfsec-skeleton/templates.go
index 7e6a527a0..2866d53db 100644
--- a/cmd/tfsec-skeleton/templates.go
+++ b/cmd/tfsec-skeleton/templates.go
@@ -2,49 +2,49 @@ package main
 
 const checkTemplate = `package {{ .Package}}
 
-import (
-	"github.com/aquasecurity/tfsec/pkg/result"
-	"github.com/aquasecurity/tfsec/pkg/severity"
-
-	"github.com/aquasecurity/tfsec/pkg/provider"
-
-	"github.com/aquasecurity/tfsec/internal/app/tfsec/hclcontext"
+// ATTENTION!
+// This rule was autogenerated!
+// Before making changes, consider updating the generator.
 
+import (
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/block"
-
-	"github.com/aquasecurity/tfsec/pkg/rule"
-
+	"github.com/aquasecurity/tfsec/internal/app/tfsec/hclcontext"
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/scanner"
+	"github.com/aquasecurity/tfsec/pkg/provider"
+	"github.com/aquasecurity/tfsec/pkg/result"
+	"github.com/aquasecurity/tfsec/pkg/rule"
+	"github.com/aquasecurity/tfsec/pkg/severity"
 )
 
-
 func init() {
 	scanner.RegisterCheckRule(rule.Rule{
+		Provider:       provider.{{.Provider.ConstName}}Provider,
 		Service:   "{{ .Service}}",
 		ShortCode: "{{ .ShortCode}}",
 		Documentation: rule.RuleDocumentation{
 			Summary:     "{{.Summary}}",
-			Explanation:` + "`" + `	` + "`" + `,
+			Explanation: ` + "`" + `{{.Explanation}}` + "`" + `,
 			Impact:      "{{.Impact}}",
 			Resolution:  "{{.Resolution}}",
 			BadExample: []string{  ` + "`" + `
-			// bad example code here
-			` + "`" + `},
+{{.BadExampleCode}}
+` + "`" + `},
 			GoodExample: []string{ ` + "`" + `
-			// good example code here
-			` + "`" + `},
+{{.GoodExampleCode}}
+` + "`" + `},
 			Links: []string{
-				
+				"{{.FirstLink}}",
 			},
 		},
-		Provider:       provider.{{.ProviderLongName}}Provider,
-		RequiredTypes:  []string{{.RequiredTypes}},
-		RequiredLabels: []string{{.RequiredLabels}},
-		DefaultSeverity: severity.Medium, 
+		RequiredTypes:  []string{ {{range .RequiredTypes}}
+			"{{.}}",{{end}}
+		},
+		RequiredLabels: []string{ {{range .RequiredLabels}}
+			"{{.}}",{{end}}
+		},
+		DefaultSeverity: severity.{{.Severity}}, 
 		CheckFunc: func(set result.Set, resourceBlock block.Block, _ *hclcontext.Context){
-				
-			// function contents here
-
+			{{.RuleCode}}
 		},
 	})
 }
@@ -52,14 +52,6 @@ func init() {
 
 const checkTestTemplate = `package {{ .Package}}
 
-import (
-	"testing"
-
-	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
-)
-
-package s3
-
 import (
 	"strings"
 	"testing"
@@ -68,17 +60,17 @@ import (
 	"github.com/aquasecurity/tfsec/internal/app/tfsec/testutil"
 )
 
-func Test_{{.CheckName}}_FailureExamples(t *testing.T) {
-	expectedCode := "{{ .FullCode}}"
+func Test_{{.TestName}}_FailureExamples(t *testing.T) {
+	expectedCode := "{{ .Provider}}-{{ .Service }}-{{.ShortCode}}"
 
-	check, err := scanner.GetRuleById(expectedCode)
+	rule, err := scanner.GetRuleById(expectedCode)
 	if err != nil {
 		t.FailNow()
 	}
-	for i, badExample := range check.Documentation.BadExample {
+	for i, badExample := range rule.Documentation.BadExample {
 		t.Logf("Running bad example for '%s' #%d", expectedCode, i+1)
 		if strings.TrimSpace(badExample) == "" {
-			t.Fatalf("bad example code not provided for %s", check.ID())
+			t.Fatalf("bad example code not provided for %s", rule.ID())
 		}
 		defer func() {
 			if err := recover(); err != nil {
@@ -86,21 +78,21 @@ func Test_{{.CheckName}}_FailureExamples(t *testing.T) {
 			}
 		}()
 		results := testutil.ScanHCL(badExample, t)
-		testutil.AssertCheckCode(t, check.ID(), "", results)
+		testutil.AssertCheckCode(t, rule.ID(), "", results)
 	}
 }
 
-func Test_{{.CheckName}}_SuccessExamples(t *testing.T) {
-	expectedCode := "{{ .FullCode}}"
+func Test_{{.TestName}}_SuccessExamples(t *testing.T) {
+	expectedCode := "{{ .Provider}}-{{ .Service }}-{{.ShortCode}}"
 
-	check, err := scanner.GetRuleById(expectedCode)
+	rule, err := scanner.GetRuleById(expectedCode)
 	if err != nil {
 		t.FailNow()
 	}
-	for i, example := range check.Documentation.GoodExample {
+	for i, example := range rule.Documentation.GoodExample {
 		t.Logf("Running good example for '%s' #%d", expectedCode, i+1)
 		if strings.TrimSpace(example) == "" {
-			t.Fatalf("good example code not provided for %s", check.ID())
+			t.Fatalf("good example code not provided for %s", rule.ID())
 		}
 		defer func() {
 			if err := recover(); err != nil {
@@ -108,7 +100,7 @@ func Test_{{.CheckName}}_SuccessExamples(t *testing.T) {
 			}
 		}()
 		results := testutil.ScanHCL(example, t)
-		testutil.AssertCheckCode(t, "", check.ID(), results)
+		testutil.AssertCheckCode(t, "", rule.ID(), results)
 	}
 }
 `
diff --git a/cmd/tfsec-skeleton/util.go b/cmd/tfsec-skeleton/util.go
deleted file mode 100644
index f90bb5c8e..000000000
--- a/cmd/tfsec-skeleton/util.go
+++ /dev/null
@@ -1,56 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"io/ioutil"
-	"os"
-	"regexp"
-	"strconv"
-	"strings"
-)
-
-func listFiles(dir, pattern string) ([]os.FileInfo, error) {
-	files, err := ioutil.ReadDir(dir)
-	if err != nil {
-		return nil, err
-	}
-
-	filteredFiles := []os.FileInfo{}
-	for _, file := range files {
-		if file.IsDir() {
-			continue
-		}
-		matched, err := regexp.MatchString(pattern, file.Name())
-		if err != nil {
-			return nil, err
-		}
-		if matched {
-			filteredFiles = append(filteredFiles, file)
-		}
-	}
-	return filteredFiles, nil
-}
-
-func calculateNextCode(provider string) (string, error) {
-	files, err := listFiles("internal/app/tfsec/rules", fmt.Sprintf("%s.*", provider))
-	if err != nil {
-		return "", err
-	}
-	re := regexp.MustCompile("[0-9]+")
-	maxCode := 0
-	for _, file := range files {
-		thisCode, _ := strconv.Atoi(strings.Join(re.FindAllString(file.Name(), -1), ""))
-		if thisCode > maxCode {
-			maxCode = thisCode
-		}
-	}
-	return fmt.Sprintf("%03d", maxCode+1), nil
-}
-
-func verifyCheckPath(checkPath string) error {
-	stat, _ := os.Stat(checkPath)
-	if stat != nil {
-		return fmt.Errorf("file [%s] already exists so not creating check", checkPath)
-	}
-	return nil
-}
diff --git a/pkg/provider/provider.go b/pkg/provider/provider.go
index 112dfec86..3dc9fbff3 100644
--- a/pkg/provider/provider.go
+++ b/pkg/provider/provider.go
@@ -21,3 +21,19 @@ const (
 func RuleProviderToString(provider Provider) string {
 	return strings.ToUpper(string(provider))
 }
+
+func (p Provider) DisplayName() string {
+	switch p {
+	case "aws":
+		return strings.ToUpper(string(p))
+	case "digitalocean":
+		return "Digital Ocean"
+	case "openstack":
+		return "OpenStack"
+	default:
+		return strings.Title(strings.ToLower(string(p)))
+	}
+}
+func (p Provider) ConstName() string {
+	return strings.ReplaceAll(p.DisplayName(), " ", "")
+}


Test patch:
diff --git a/cmd/tfsec-skeleton/examples/mutate_test.go b/cmd/tfsec-skeleton/examples/mutate_test.go
new file mode 100644
index 000000000..11e51ddb0
--- /dev/null
+++ b/cmd/tfsec-skeleton/examples/mutate_test.go
@@ -0,0 +1,103 @@
+package examples
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_Mutation(t *testing.T) {
+	table := []struct {
+		input  string
+		path   string
+		value  interface{}
+		output string
+	}{
+		{
+			input: `
+resource "google_project" "something" {
+	auto_create_network = true	
+}
+			`,
+			path:  "resource.google_project.*.auto_create_network",
+			value: false,
+			output: `
+resource "google_project" "example" {
+	auto_create_network = false
+}
+			`,
+		},
+		{
+			input: `
+resource "google_project" "something" {
+	name = "something"
+}
+			`,
+			path:  "resource.google_project.*.auto_create_network",
+			value: false,
+			output: `
+resource "google_project" "example" {
+	name = "something"
+	auto_create_network = false
+}
+			`,
+		},
+		{
+			input: `
+resource "computer" "something" {
+	name = "something"
+	settings {
+		security {
+			encryption {
+				enabled = false
+			}
+		}
+	}
+}
+			`,
+			path:  "resource.computer.*.settings.security.encryption.enabled",
+			value: true,
+			output: `
+resource "computer" "example" {
+	name = "something"
+	settings {
+		security {
+			encryption {
+				enabled = true
+			}
+		}
+	}
+}
+			`,
+		},
+		{
+			input: `
+resource "computer" "something" {
+	name = "something"
+}
+			`,
+			path:  "resource.computer.*.settings.security.encryption.enabled",
+			value: true,
+			output: `
+resource "computer" "example" {
+	name = "something"
+	settings {
+		security {
+			encryption {
+				enabled = true
+			}
+		}
+	}
+}
+			`,
+		},
+	}
+
+	for i, test := range table {
+		t.Run(fmt.Sprintf("Case #%d", i), func(t *testing.T) {
+			actual := SetAttribute(test.input, test.path, test.value, "example")
+			assert.Equal(t, test.output, actual)
+		})
+	}
+}
diff --git a/cmd/tfsec-skeleton/requirements/attributes_test.go b/cmd/tfsec-skeleton/requirements/attributes_test.go
new file mode 100644
index 000000000..635f767cd
--- /dev/null
+++ b/cmd/tfsec-skeleton/requirements/attributes_test.go
@@ -0,0 +1,71 @@
+package requirements
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_AttributeMustHaveValue(t *testing.T) {
+	requirement := AttributeMustHaveValue("resource", "made_up_resource", "settings.encryption.enabled", true, true, "")
+
+	assert.Equal(t, `
+resource "made_up_resource" "good_example" {
+	settings {
+		encryption {
+			enabled = true
+		}
+	}
+}
+`, requirement.GenerateGoodExample())
+
+	assert.Equal(t, `
+resource "made_up_resource" "bad_example" {
+	settings {
+		encryption {
+			enabled = false
+		}
+	}
+}
+`, requirement.GenerateBadExample())
+
+	assert.Equal(t, `if enabledAttr := resourceBlock.GetBlock("settings").GetBlock("encryption").GetAttribute("enabled"); enabledAttr.IsNil() { // alert on use of default value
+				set.AddResult().
+					WithDescription("Resource '%s' uses default value for settings.encryption.enabled", resourceBlock.FullName())
+			} else if enabledAttr.IsFalse() {
+				set.AddResult().
+					WithDescription("Resource '%s' does not have settings.encryption.enabled set to true", resourceBlock.FullName()).
+					WithAttribute(enabledAttr)
+			}`, requirement.GenerateRuleCode())
+
+}
+
+func Test_AttributeMustNotHaveValue(t *testing.T) {
+	requirement := AttributeMustNotHaveValue("resource", "made_up_resource", "settings.lucky_number", 13, true, "")
+
+	assert.Equal(t, `
+resource "made_up_resource" "good_example" {
+	settings {
+		lucky_number = 14
+	}
+}
+`, requirement.GenerateGoodExample())
+
+	assert.Equal(t, `
+resource "made_up_resource" "bad_example" {
+	settings {
+		lucky_number = 13
+	}
+}
+`, requirement.GenerateBadExample())
+
+	assert.Equal(t, `if luckyNumberAttr := resourceBlock.GetBlock("settings").GetAttribute("lucky_number"); luckyNumberAttr.IsNil() { // alert on use of default value
+				set.AddResult().
+					WithDescription("Resource '%s' uses default value for settings.lucky_number", resourceBlock.FullName())
+			} else if luckyNumberAttr.Equals(13) {
+				set.AddResult().
+					WithDescription("Resource '%s' has settings.lucky_number set to 13", resourceBlock.FullName()).
+					WithAttribute(luckyNumberAttr)
+			}`, requirement.GenerateRuleCode())
+
+}