Skip to content

Commit

Permalink
Merge pull request #2 from RoseSecurity/rework-to-graph
Browse files Browse the repository at this point in the history
Rework to utilize Terraform graph
  • Loading branch information
RoseSecurity authored Jun 5, 2024
2 parents f14c42d + de99616 commit 9eb1e65
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 129 deletions.
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,16 @@ make build

## Usage

1. We will generate a Terraform plan file, specifically `tf_plan_prod.json`:
1. Run the program from the directory containing your Terraform:

```sh
terraform plan -out tf_plan_prod
terramaid
```

2. After generating the plan file, we will convert it to JSON using Terraform show:
> [!NOTE]
> If your Terraform binary is not located at `/usr/local/bin/terraform`, you will have to provide the path to the binary. For example, on Mac, this location could be: `/opt/homebrew/bin/terraform`
```sh
terraform show -json tf_plan_prod > tf_plan_prod.json
```

3. Once the JSON plan file has been created, run `terramaid` against it and look for the populated `Terramaid.md` file!

```sh
terramaid -planfile tf_plan_prod.json
```
3. Look for the populated `Terramaid.md` file!

```sh
cat Terramaid.md
Expand All @@ -59,9 +52,17 @@ docker run -it -v $(pwd):/usr/src/terramaid rosesecurity/terramaid:latest -planf
**Output:**

```mermaid
graph TD;
67(aws_iam_policy) -->|created| 68(aws_iam_policy.policy)
69(aws_s3_bucket) -->|created| 70(aws_s3_bucket.this)
flowchart TD;
subgraph Terraform
aws_db_instance.dev_example_db_instance["aws_db_instance.dev_example_db_instance"]
aws_instance.dev_example_instance["aws_instance.dev_example_instance"]
aws_s3_bucket.dev_logs_bucket["aws_s3_bucket.dev_logs_bucket"]
aws_s3_bucket.dev_test_bucket["aws_s3_bucket.dev_test_bucket"]
aws_s3_bucket_policy.dev_logs_bucket_policy["aws_s3_bucket_policy.dev_logs_bucket_policy"]
aws_s3_bucket_policy.dev_test_bucket_policy["aws_s3_bucket_policy.dev_test_bucket_policy"]
aws_s3_bucket_policy.dev_logs_bucket_policy --> aws_s3_bucket.dev_logs_bucket
aws_s3_bucket_policy.dev_test_bucket_policy --> aws_s3_bucket.dev_test_bucket
end
```

## CI/CD Integration
Expand Down Expand Up @@ -97,16 +98,10 @@ jobs:
- name: Init
run: terraform init

- name: Plan
run: terraform plan -out=tfplan

- name: JSON Plan
run: terraform show -json tfplan > tfplan.json

- name: Terramaid
id: terramaid
run: |
./usr/local/bin/terramaid tfplan.json
./usr/local/bin/terramaid
- name: Upload comment to PR
uses: actions/github-script@v6
Expand Down
8 changes: 1 addition & 7 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,4 @@ module github.com/RoseSecurity/terramaid

go 1.22.2

require github.com/fatih/color v1.17.0

require (
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.18.0 // indirect
)
require github.com/awalterschulze/gographviz v2.0.3+incompatible
13 changes: 2 additions & 11 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,11 +1,2 @@
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
github.com/awalterschulze/gographviz v2.0.3+incompatible h1:9sVEXJBJLwGX7EQVhLm2elIKCm7P2YHFC8v6096G09E=
github.com/awalterschulze/gographviz v2.0.3+incompatible/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
132 changes: 43 additions & 89 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,114 +1,68 @@
package main

import (
"encoding/json"
"flag"
"fmt"
"log"
"os"
"os/exec"
"strings"

"github.com/fatih/color"
"github.com/awalterschulze/gographviz"
)

// Define structures to parse Terraform plan JSON
type Plan struct {
ResourceChanges []ResourceChange `json:"resource_changes"`
}

type ResourceChange struct {
Address string `json:"address"`
Type string `json:"type"`
Change Change `json:"change"`
}

type Change struct {
Actions []string `json:"actions"`
}

func main() {
// Read and parse plan file
planfile := flag.String("planfile", "tfplan.json", "path to the Terraform plan file")
var tfPath string
flag.StringVar(&tfPath, "tfPath", "/usr/local/bin/terraform", "Path to Terraform binary")
flag.Parse()

var data []byte
var err error

if *planfile == "" {
red := color.New(color.FgRed, color.Bold)
red.Println("Error: No plan file provided. Please provide a file using the -planfile flag.")
os.Exit(1)
} else {
data, err = os.ReadFile(*planfile)
if err != nil {
red := color.New(color.FgRed, color.Bold)
red.Printf("Error reading plan file: %v\n", err)
os.Exit(1)
}
}

var plan Plan
err = json.Unmarshal(data, &plan)
// Run terraform graph command
cmd := exec.Command(tfPath, "graph")
output, err := cmd.Output()
if err != nil {
log.Fatalf("Error parsing plan file: %v\n", err)
fmt.Println("Error running terraform graph command", err)
return
}

// Write the Mermaid diagram to the output file
outFile, err := os.Create("Terramaid.md")
// Parse the DOT output
dot := string(output)
graphAst, err := gographviz.ParseString(dot)
if err != nil {
log.Fatalf("Error creating output file: %v\n", err)
} else {
fmt.Println("Terramaid file created")
defer outFile.Close()
fmt.Println("Error parsing DOT:", err)
return
}

fmt.Fprintln(outFile, "```mermaid")
fmt.Fprintln(outFile, "graph TD;")

// Create a map to keep track of node names and their assigned variable
nodeMap := make(map[string]string)
varNameCounter := 0

getVarName := func() string {
varName := fmt.Sprint('A' + varNameCounter)
varNameCounter++
return varName
graph := gographviz.NewGraph()
if err := gographviz.Analyse(graphAst, graph); err != nil {
fmt.Println("Error analyzing graph:", err)
return
}

for _, rc := range plan.ResourceChanges {
// Assign or retrieve variable names for the nodes
sourceKey := rc.Type
targetKey := rc.Address

if _, exists := nodeMap[sourceKey]; !exists {
nodeMap[sourceKey] = getVarName()
}
if _, exists := nodeMap[targetKey]; !exists {
nodeMap[targetKey] = getVarName()
}

sourceVar := nodeMap[sourceKey]
targetVar := nodeMap[targetKey]

source := fmt.Sprintf("%s(%s)", sourceVar, rc.Type)
target := fmt.Sprintf("%s(%s)", targetVar, rc.Address)

if contains(rc.Change.Actions, "create") {
fmt.Fprintf(outFile, "%s -->|created| %s\n", source, target)
} else if contains(rc.Change.Actions, "update") {
fmt.Fprintf(outFile, "%s -->|updated| %s\n", source, target)
} else if contains(rc.Change.Actions, "delete") {
fmt.Fprintf(outFile, "%s -->|deleted| %s\n", source, target)
}
// Convert to Mermaid format
mermaidGraph := ConvertToMermaid(graph)
err = os.WriteFile("Terramaid.md", []byte(mermaidGraph), 0644)
if err != nil {
fmt.Println("Error writing to Terramaid file:", err)
return
}
fmt.Fprintln(outFile, "```")
}
func ConvertToMermaid(graph *gographviz.Graph) string {
var sb strings.Builder

sb.WriteString("```mermaid\n")
sb.WriteString("flowchart TD;\n")
sb.WriteString("\tsubgraph Terraform\n")
for _, node := range graph.Nodes.Nodes {
label := strings.Trim(node.Attrs["label"], "\"")
nodeName := strings.Trim(node.Name, "\"")
sb.WriteString(fmt.Sprintf(" %s[\"%s\"]\n", nodeName, label))
}

// Helper function to check if a slice contains a string
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
for _, edge := range graph.Edges.Edges {
srcName := strings.Trim(edge.Src, "\"")
dstName := strings.Trim(edge.Dst, "\"")
sb.WriteString(fmt.Sprintf(" %s --> %s\n", srcName, dstName))
}
return false
sb.WriteString("\tend\n```\n")

return sb.String()
}

0 comments on commit 9eb1e65

Please sign in to comment.