Skip to content

Commit e74c606

Browse files
authored
Merge pull request #38 from RoseSecurity/add-support-for-higher-counts-of-resources
Update Tests, Provider Name Cleanup, and Flowchart Functions
2 parents 7ec4818 + 5cfede6 commit e74c606

File tree

18 files changed

+567
-141
lines changed

18 files changed

+567
-141
lines changed

.github/workflows/build.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: "Build CLI and attach to GitHub release"
1+
name: "Build CLI and Attach to GitHub Release"
22

33
on:
44
release:
@@ -60,6 +60,10 @@ jobs:
6060
push: true
6161
tags: rosesecurity/terramaid:latest
6262

63+
- name: "Verify Image"
64+
run: |
65+
docker pull ${{ steps.build.outputs.image }}:${{ steps.build.outputs.tag}}
66+
6367
homebrew:
6468
name: "Bump Homebrew Formula"
6569
runs-on: ubuntu-latest
@@ -72,4 +76,3 @@ jobs:
7276
formula-path: Formula/t/terramaid.rb
7377
env:
7478
COMMITTER_TOKEN: ${{ secrets.COMMITTER_TOKEN }}
75-

.github/workflows/test.yml

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Test
1+
name: "Test Terramaid Outputs"
22

33
on:
44
pull_request:
@@ -7,16 +7,42 @@ on:
77
- main
88

99
jobs:
10-
test:
10+
test_aws:
1111
runs-on: ubuntu-latest
1212
steps:
1313
- uses: actions/checkout@v4
1414
- uses: hashicorp/setup-terraform@v3
1515
- uses: actions/setup-go@v5
1616
with:
17-
go-version: '1.22.2'
17+
go-version: "1.22.2"
1818
cache: false
1919
- run: |
2020
make build
21-
build/terramaid -w test/
21+
build/terramaid -w test/aws
22+
cat Terramaid.md
23+
test_gcp:
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: hashicorp/setup-terraform@v3
28+
- uses: actions/setup-go@v5
29+
with:
30+
go-version: "1.22.2"
31+
cache: false
32+
- run: |
33+
make build
34+
build/terramaid -w test/gcp
35+
cat Terramaid.md
36+
test_az:
37+
runs-on: ubuntu-latest
38+
steps:
39+
- uses: actions/checkout@v4
40+
- uses: hashicorp/setup-terraform@v3
41+
- uses: actions/setup-go@v5
42+
with:
43+
go-version: "1.22.2"
44+
cache: false
45+
- run: |
46+
make build
47+
build/terramaid -w test/az
2248
cat Terramaid.md

.goreleaser.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ before:
99
builds:
1010
- env:
1111
- CGO_ENABLED=0
12-
ldflags: -s -w -X main.version={{ .Version }}
12+
ldflags:
13+
- '-s -w -X "github.com/rosesecurity/Terramaid/cmd.Version={{.Version}}"'
1314
goos:
1415
- linux
1516
- windows

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fmt: ## Format Go files
1313
gofumpt -w .
1414

1515
build: ## Build Terramaid
16-
$(GO) build -ldflags="-s -w" -o build/$(BINARY_NAME) main.go
16+
env $(if $(GOOS),GOOS=$(GOOS)) $(if $(GOARCH),GOARCH=$(GOARCH)) $(GO) build -o build/$(BINARY_NAME) -ldflags "-X 'github.com/rosesecurity/Terramaid/cmd.Version=${VERSION}'" main.go
1717

1818
install: ## Install dependencies
1919
$(GO) install ./...@latest

README.md

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,39 @@ Terramaid transforms your Terraform resources and plans into visually appealing
2222

2323
```mermaid
2424
flowchart TD
25-
subgraph Hashicorp
26-
subgraph Terraform
27-
aws_db_instance.example_db["aws_db_instance.example_db"]
28-
aws_instance.example_instance["aws_instance.example_instance"]
29-
aws_s3_bucket.logs["aws_s3_bucket.logs"]
30-
aws_s3_bucket.test["aws_s3_bucket.test"]
31-
aws_s3_bucket_policy.logs_policy["aws_s3_bucket_policy.logs_policy"]
32-
aws_s3_bucket_policy.test_policy["aws_s3_bucket_policy.test_policy"]
33-
aws_s3_bucket_policy.logs_policy --> aws_s3_bucket.logs
34-
aws_s3_bucket_policy.test_policy --> aws_s3_bucket.test
35-
end
36-
end
25+
subgraph Terraform
26+
subgraph Aws
27+
aws_db_instance.main_db["aws_db_instance.main_db"]
28+
aws_instance.app_server["aws_instance.app_server"]
29+
aws_instance.web_server["aws_instance.web_server"]
30+
aws_lb.web["aws_lb.web"]
31+
aws_lb_listener.web["aws_lb_listener.web"]
32+
aws_lb_target_group.web["aws_lb_target_group.web"]
33+
aws_lb_target_group_attachment.web["aws_lb_target_group_attachment.web"]
34+
aws_s3_bucket.logs["aws_s3_bucket.logs"]
35+
aws_s3_bucket.test["aws_s3_bucket.test"]
36+
aws_s3_bucket_policy.logs_policy["aws_s3_bucket_policy.logs_policy"]
37+
aws_s3_bucket_policy.test_policy["aws_s3_bucket_policy.test_policy"]
38+
aws_security_group.db["aws_security_group.db"]
39+
aws_security_group.web["aws_security_group.web"]
40+
aws_subnet.private["aws_subnet.private"]
41+
aws_subnet.public["aws_subnet.public"]
42+
aws_vpc.main["aws_vpc.main"]
43+
end
44+
aws_lb.web --> aws_security_group.web
45+
aws_lb.web --> aws_subnet.public
46+
aws_lb_listener.web --> aws_lb.web
47+
aws_lb_listener.web --> aws_lb_target_group.web
48+
aws_lb_target_group.web --> aws_vpc.main
49+
aws_lb_target_group_attachment.web --> aws_instance.web_server
50+
aws_lb_target_group_attachment.web --> aws_lb_target_group.web
51+
aws_s3_bucket_policy.logs_policy --> aws_s3_bucket.logs
52+
aws_s3_bucket_policy.test_policy --> aws_s3_bucket.test
53+
aws_security_group.db --> aws_security_group.web
54+
aws_security_group.web --> aws_vpc.main
55+
aws_subnet.private --> aws_vpc.main
56+
aws_subnet.public --> aws_vpc.main
57+
end
3758
```
3859

3960
> [!TIP]

internal/flowchart.go

Lines changed: 48 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
)
1212

1313
type Node struct {
14-
ID string
15-
Label string
14+
ID string
15+
Label string
16+
Count int
17+
Provider string
1618
}
1719

1820
type Edge struct {
@@ -21,114 +23,118 @@ type Edge struct {
2123
}
2224

2325
type Graph struct {
24-
Nodes []Node
25-
Edges []Edge
26+
Nodes []Node
27+
Edges []Edge
28+
NodeMap map[string]int
2629
}
2730

28-
// Removes unnecessary parts from the label
31+
var labelCleaner = regexp.MustCompile(`\s*\(expand\)|\s*\(close\)|\[root\]\s*|"`)
32+
33+
// CleanLabel removes unnecessary parts from the label
2934
func CleanLabel(label string) string {
30-
re := regexp.MustCompile(`\s*\(expand\)|\s*\(close\)|\[root\]\s*|"`)
31-
return re.ReplaceAllString(label, "")
35+
return labelCleaner.ReplaceAllString(label, "")
3236
}
3337

34-
// Removes unnecessary parts from the ID
38+
// CleanID removes unnecessary parts from the ID
3539
func CleanID(id string) string {
36-
re := regexp.MustCompile(`\s*\(expand\)|\s*\(close\)|\[root\]\s*|"`)
37-
return re.ReplaceAllString(id, "")
40+
return labelCleaner.ReplaceAllString(id, "")
3841
}
3942

40-
// Extracts the provider for separate subgraph
43+
// ExtractProvider extracts the provider for separate subgraph
4144
func ExtractProvider(label string) string {
42-
if strings.Contains(label, "provider") {
43-
parts := strings.Split(label, "/")
44-
if len(parts) > 2 {
45-
return parts[len(parts)-2]
46-
}
45+
parts := strings.Split(label, "_")
46+
if len(parts) > 0 {
47+
// Remove quotes from the provider name
48+
return strings.ReplaceAll(parts[0], "\"", "")
4749
}
4850
return ""
4951
}
5052

51-
// Transforms the parsed graph into cleaned nodes and edges
53+
// TransformGraph transforms the parsed graph into cleaned nodes and edges
5254
func TransformGraph(graph *gographviz.Graph) Graph {
5355
nodes := []Node{}
5456
edges := []Edge{}
57+
nodeMap := make(map[string]int)
5558

5659
for _, node := range graph.Nodes.Nodes {
5760
cleanedID := CleanID(node.Name)
5861
cleanedLabel := CleanLabel(node.Attrs["label"])
59-
if cleanedLabel != "" && !strings.Contains(cleanedLabel, "provider") {
60-
nodes = append(nodes, Node{ID: cleanedID, Label: cleanedLabel})
62+
provider := ExtractProvider(cleanedLabel)
63+
if cleanedLabel != "" {
64+
nodeMap[cleanedLabel]++
65+
nodes = append(nodes, Node{ID: cleanedID, Label: cleanedLabel, Count: nodeMap[cleanedLabel], Provider: provider})
6166
}
6267
}
6368

6469
for _, edge := range graph.Edges.Edges {
6570
fromLabel := CleanLabel(graph.Nodes.Lookup[edge.Src].Attrs["label"])
6671
toLabel := CleanLabel(graph.Nodes.Lookup[edge.Dst].Attrs["label"])
67-
if fromLabel != "" && toLabel != "" && !strings.Contains(fromLabel, "provider") && !strings.Contains(toLabel, "provider") {
72+
if fromLabel != "" && toLabel != "" {
6873
edges = append(edges, Edge{From: CleanID(edge.Src), To: CleanID(edge.Dst)})
6974
}
7075
}
7176

72-
return Graph{Nodes: nodes, Edges: edges}
77+
return Graph{Nodes: nodes, Edges: edges, NodeMap: nodeMap}
7378
}
7479

75-
// Converts a gographviz graph to a Mermaid.js compatible string.
80+
// ConvertToMermaidFlowchart converts a gographviz graph to a Mermaid.js compatible string.
7681
// It accepts a graph, direction, and an optional subgraph name.
7782
func ConvertToMermaidFlowchart(graph *gographviz.Graph, direction string, subgraphName string) (string, error) {
7883
var sb strings.Builder
7984

80-
// Capitalize the provider name
8185
caser := cases.Title(language.English)
82-
// Validate the direction of the flowchart. Valid options are: TB, TD, BT, RL, LR
8386
validDirections := map[string]bool{
8487
"TB": true, "TD": true, "BT": true, "RL": true, "LR": true,
8588
}
8689
if !validDirections[direction] {
8790
return "", fmt.Errorf("invalid direction %s: valid options are: TB, TD, BT, RL, LR", direction)
8891
}
89-
// Start Mermaid graph definition
92+
9093
sb.WriteString("```mermaid\n")
9194
sb.WriteString("flowchart " + direction + "\n")
9295

93-
// Add subgraph for providers
96+
if subgraphName != "" {
97+
sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", subgraphName))
98+
}
99+
94100
providerSubgraphs := make(map[string]bool)
95101
for _, n := range graph.Nodes.Nodes {
96-
label := n.Attrs["label"]
97-
provider := ExtractProvider(label)
102+
provider := ExtractProvider(n.Attrs["label"])
98103
if provider != "" && !providerSubgraphs[provider] {
99-
sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", caser.String(provider)))
104+
sb.WriteString(fmt.Sprintf("\t\tsubgraph %s\n", caser.String(provider)))
100105
providerSubgraphs[provider] = true
101106
}
102107
}
103108

104-
if subgraphName != "" {
105-
sb.WriteString(fmt.Sprintf("\tsubgraph %s\n", subgraphName))
106-
}
107-
108-
// Iterate over nodes to add them to the Mermaid graph
109+
nodeMap := make(map[string]int)
109110
for _, n := range graph.Nodes.Nodes {
110111
label := CleanLabel(n.Attrs["label"])
111112
nodeName := CleanID(n.Name)
112-
if label != "" && nodeName != "" && !strings.Contains(label, "provider") {
113-
sb.WriteString(fmt.Sprintf("\t\t%s[\"%s\"]\n", nodeName, label))
113+
if label != "" && nodeName != "" {
114+
nodeMap[label]++
115+
count := nodeMap[label]
116+
if count > 1 {
117+
sb.WriteString(fmt.Sprintf("\t\t\t%s[\"%s\\nCount: %d\"]\n", nodeName, label, count))
118+
} else {
119+
sb.WriteString(fmt.Sprintf("\t\t\t%s[\"%s\"]\n", nodeName, label))
120+
}
114121
}
115122
}
116123

117-
// Iterate over edges to add them to the Mermaid graph
124+
for range providerSubgraphs {
125+
sb.WriteString("\t\tend\n")
126+
}
127+
118128
for _, edge := range graph.Edges.Edges {
119129
srcLabel := CleanLabel(graph.Nodes.Lookup[edge.Src].Attrs["label"])
120130
dstLabel := CleanLabel(graph.Nodes.Lookup[edge.Dst].Attrs["label"])
121131
srcName := CleanID(edge.Src)
122132
dstName := CleanID(edge.Dst)
123-
if srcLabel != "" && dstLabel != "" && !strings.Contains(srcLabel, "provider") && !strings.Contains(dstLabel, "provider") {
133+
if srcLabel != "" && dstLabel != "" {
124134
sb.WriteString(fmt.Sprintf("\t\t%s --> %s\n", srcName, dstName))
125135
}
126136
}
127137

128-
// Close all open subgraphs
129-
for range providerSubgraphs {
130-
sb.WriteString("\tend\n")
131-
}
132138
if subgraphName != "" {
133139
sb.WriteString("\tend\n")
134140
}

main.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@ import (
55
u "github.com/RoseSecurity/terramaid/pkg/utils"
66
)
77

8-
var version string
9-
108
func main() {
11-
cmd.Version = version
12-
139
if err := cmd.Execute(); err != nil {
1410
u.LogErrorAndExit(err)
1511
}

0 commit comments

Comments
 (0)