Skip to content

Commit 0c7da96

Browse files
eversCosterman
andauthored
Add tf 0 12 support (#12)
* Add tf 0.12 support * Ensure currentResource is maintained over scanning * More versioning of regexes * Add comments * PR fix for random_string.some_password.id * Update main.go Co-authored-by: Erik Osterman <erik@cloudposse.com>
1 parent 7a4a942 commit 0c7da96

File tree

2 files changed

+329
-62
lines changed

2 files changed

+329
-62
lines changed

main.go

Lines changed: 160 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,28 @@ import (
1111
"unicode/utf8"
1212
)
1313

14+
type match struct {
15+
leadingWhitespace string
16+
property string // something like `stage.0.action.0.configuration.OAuthToken`
17+
trailingWhitespace string
18+
firstQuote string // < or "
19+
oldValue string
20+
secondQuote string // > or "
21+
thirdQuote string // < or " or (
22+
newValue string
23+
fourthQuote string // > or " or )
24+
postfix string
25+
}
26+
27+
type expression struct {
28+
planStatusRegex *regexp.Regexp
29+
reTfPlanLine *regexp.Regexp
30+
reTfPlanCurrentResource *regexp.Regexp
31+
resourceIndex int
32+
assign string
33+
operator string
34+
}
35+
1436
func init() {
1537
// make sure we only have one process and that it runs on the main thread
1638
// (so that ideally, when we Exec, we keep our user switches and stuff)
@@ -25,86 +47,162 @@ func getEnv(key, fallback string) string {
2547
return fallback
2648
}
2749

50+
var versionedExpressions = map[string]expression{
51+
"0.11": expression{
52+
planStatusRegex: regexp.MustCompile(
53+
"^(.*?): (.*?) +\\(ID: (.*?)\\)$",
54+
),
55+
reTfPlanLine: regexp.MustCompile(
56+
"^( +)([a-zA-Z0-9%._-]+):( +)([\"<])(.*?)([>\"]) +=> +([\"<])(.*)([>\"])(.*)$",
57+
),
58+
reTfPlanCurrentResource: regexp.MustCompile(
59+
"^([~/+-]+) (.*?) +(.*)$",
60+
),
61+
resourceIndex: 2,
62+
assign: ":",
63+
operator: "=>",
64+
},
65+
"0.12": expression{
66+
planStatusRegex: regexp.MustCompile(
67+
"^(.*?): (.*?) +\\[id=(.*?)\\]$",
68+
),
69+
reTfPlanLine: regexp.MustCompile(
70+
"^( +)([ ~a-zA-Z0-9%._-]+)=( +)([\"<])(.*?)([>\"]) +-> +(\\()(.*)(\\))(.*)$",
71+
),
72+
reTfPlanCurrentResource: regexp.MustCompile(
73+
"^([~/+-]+) (.*?) +(.*) (.*) (.*)$",
74+
),
75+
resourceIndex: 3,
76+
assign: "=",
77+
operator: "->",
78+
},
79+
}
80+
2881
func main() {
2982
log.SetFlags(0) // no timestamps on our logs
3083

3184
// Character used to mask sensitive output
3285
var tfmaskChar = getEnv("TFMASK_CHAR", "*")
33-
3486
// Pattern representing sensitive output
35-
var tfmaskValuesRegex = getEnv("TFMASK_VALUES_REGEX", "(?i)^.*(oauth|secret|token|password|key|result).*$")
36-
87+
var tfmaskValuesRegex = getEnv("TFMASK_VALUES_REGEX",
88+
"(?i)^.*(oauth|secret|token|password|key|result|id).*$")
3789
// Pattern representing sensitive resource
38-
var tfmaskResourceRegex = getEnv("TFMASK_RESOURCES_REGEX", "(?i)^(random_id).*$")
39-
40-
// stage.0.action.0.configuration.OAuthToken: "" => "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
41-
reTfPlanLine := regexp.MustCompile("^( +)([a-zA-Z0-9%._-]+):( +)([\"<])(.*?)([>\"]) +=> +([\"<])(.*)([>\"])(.*)$")
42-
43-
// random_id.some_id: Refreshing state... (ID: itILf4x5lqleQV9ZwT2gH-Zg3yuXM8pdUu6VFTX...P5vqUmggDweOoxFMPY5t9thA0SJE2EZIhcHbsQ)
44-
reTfPlanStatusLine := regexp.MustCompile("^(.*?): (.*?) +\\(ID: (.*?)\\)$")
45-
90+
var tfmaskResourceRegex = getEnv("TFMASK_RESOURCES_REGEX",
91+
"(?i)^(random_id|random_string).*$")
4692

47-
// -/+ random_string.postgres_admin_password (tainted) (new resource required)
48-
reTfPlanCurrentResource := regexp.MustCompile("^([~/+-]+) (.*?) +(.*)$")
49-
reTfApplyCurrentResource := regexp.MustCompile("^([a-z].*?): (.*?)$")
50-
currentResource := ""
93+
// Default to tf 0.11, but users can override
94+
var tfenv = getEnv("TFENV", "0.12")
5195

5296
reTfValues := regexp.MustCompile(tfmaskValuesRegex)
5397
reTfResource := regexp.MustCompile(tfmaskResourceRegex)
5498
scanner := bufio.NewScanner(os.Stdin)
99+
versionedExpressions := versionedExpressions[tfenv]
100+
// initialise currentResource once before scanning
101+
currentResource := ""
55102
for scanner.Scan() {
56103
line := scanner.Text()
57-
if reTfPlanCurrentResource.MatchString(line) {
58-
match := reTfPlanCurrentResource.FindStringSubmatch(line)
59-
currentResource = match[2]
60-
} else if reTfApplyCurrentResource.MatchString(line) {
61-
match := reTfApplyCurrentResource.FindStringSubmatch(line)
62-
currentResource = match[1]
63-
}
64-
65-
if reTfPlanStatusLine.MatchString(line) {
66-
match := reTfPlanStatusLine.FindStringSubmatch(line)
67-
resource := match[1]
68-
id := match[3]
69-
if reTfResource.MatchString(resource) {
70-
line = strings.Replace(line, id, strings.Repeat(tfmaskChar, utf8.RuneCountInString(id)), 1)
71-
}
72-
fmt.Println(line)
73-
} else if reTfPlanLine.MatchString(line) {
74-
match := reTfPlanLine.FindStringSubmatch(line)
75-
leadingWhitespace := match[1]
76-
property := match[2] // something like `stage.0.action.0.configuration.OAuthToken`
77-
trailingWhitespace := match[3]
78-
firstQuote := match[4] // < or "
79-
oldValue := match[5]
80-
secondQuote := match[6] // > or "
81-
thirdQuote := match[7] // < or "
82-
newValue := match[8]
83-
fourthQuote := match[9] // > or "
84-
postfix := match[10]
85-
86-
if reTfValues.MatchString(property) || reTfResource.MatchString(currentResource) {
87-
// The value inside the "..." or <...>
88-
if oldValue != "sensitive" && oldValue != "computed" && oldValue != "<computed" {
89-
oldValue = strings.Repeat(tfmaskChar, utf8.RuneCountInString(oldValue))
90-
}
91-
// The value inside the "..." or <...>
92-
if newValue != "sensitive" && newValue != "computed" && newValue != "<computed" {
93-
newValue = strings.Repeat(tfmaskChar, utf8.RuneCountInString(newValue))
94-
}
95-
fmt.Printf("%v%v:%v%v%v%v => %v%v%v%v\n",
96-
leadingWhitespace, property, trailingWhitespace, firstQuote, oldValue, secondQuote, thirdQuote, newValue, fourthQuote, postfix)
97-
} else {
98-
fmt.Println(line)
99-
}
100-
} else {
101-
// We matched nothing
102-
fmt.Println(line)
103-
}
104+
currentResource = getCurrentResource(versionedExpressions,
105+
currentResource, line)
106+
fmt.Println(processLine(versionedExpressions, reTfResource, reTfValues,
107+
tfmaskChar, currentResource, line))
104108
}
105109

106110
if err := scanner.Err(); err != nil {
107111
fmt.Fprintln(os.Stderr, "error:", err)
108112
os.Exit(1)
109113
}
110114
}
115+
116+
func getCurrentResource(expression expression, currentResource, line string) string {
117+
reTfApplyCurrentResource := regexp.MustCompile("^([a-z].*?): (.*?)$")
118+
if expression.reTfPlanCurrentResource.MatchString(line) {
119+
match := expression.reTfPlanCurrentResource.FindStringSubmatch(line)
120+
// for tf 0.12 the resource is wrapped in quotes, so remove them
121+
strippedResource := strings.Replace(match[expression.resourceIndex],
122+
"\"", "", -1)
123+
currentResource = strippedResource
124+
} else if reTfApplyCurrentResource.MatchString(line) {
125+
match := reTfApplyCurrentResource.FindStringSubmatch(line)
126+
currentResource = match[1]
127+
}
128+
return currentResource
129+
}
130+
131+
func processLine(expression expression, reTfResource,
132+
reTfValues *regexp.Regexp, tfmaskChar, currentResource,
133+
line string) string {
134+
if expression.planStatusRegex.MatchString(line) {
135+
line = planStatus(expression.planStatusRegex, reTfResource, tfmaskChar,
136+
line)
137+
} else if expression.reTfPlanLine.MatchString(line) {
138+
line = planLine(expression.reTfPlanLine, reTfResource, reTfValues,
139+
currentResource, tfmaskChar, expression.assign,
140+
expression.operator, line)
141+
}
142+
return line
143+
}
144+
145+
func planStatus(planStatusRegex, reTfResource *regexp.Regexp, tfmaskChar,
146+
line string) string {
147+
match := planStatusRegex.FindStringSubmatch(line)
148+
resource := match[1]
149+
id := match[3]
150+
if reTfResource.MatchString(resource) {
151+
line = strings.Replace(line, id, strings.Repeat(tfmaskChar,
152+
utf8.RuneCountInString(id)), 1)
153+
}
154+
return line
155+
}
156+
157+
func matchFromLine(reTfPlanLine *regexp.Regexp, line string) match {
158+
subMatch := reTfPlanLine.FindStringSubmatch(line)
159+
return match{
160+
leadingWhitespace: subMatch[1],
161+
property: subMatch[2], // something like `stage.0.action.0.configuration.OAuthToken`
162+
trailingWhitespace: subMatch[3],
163+
firstQuote: subMatch[4],
164+
oldValue: subMatch[5],
165+
secondQuote: subMatch[6], // > or "
166+
thirdQuote: subMatch[7], // < or " or (
167+
newValue: subMatch[8],
168+
fourthQuote: subMatch[9], // > or " or )
169+
postfix: subMatch[10],
170+
}
171+
}
172+
173+
func planLine(reTfPlanLine, reTfResource, reTfValues *regexp.Regexp,
174+
currentResource, tfmaskChar, assign, operator, line string) string {
175+
match := matchFromLine(reTfPlanLine, line)
176+
if reTfValues.MatchString(match.property) ||
177+
reTfResource.MatchString(currentResource) {
178+
// The value inside the "...", <...> or (...)
179+
oldValue := maskValue(match.oldValue, tfmaskChar)
180+
// The value inside the "...", <...> or (...)
181+
newValue := maskValue(match.newValue, tfmaskChar)
182+
line = fmt.Sprintf("%v%v%v%v%v%v%v %v %v%v%v%v",
183+
match.leadingWhitespace, match.property, assign,
184+
match.trailingWhitespace, match.firstQuote, oldValue,
185+
match.secondQuote, operator, match.thirdQuote,
186+
newValue, match.fourthQuote, match.postfix)
187+
}
188+
return line
189+
}
190+
191+
func maskValue(value, tfmaskChar string) string {
192+
exclusions := []string{"sensitive", "computed", "<computed",
193+
"known after apply"}
194+
if !contains(exclusions, value) {
195+
return strings.Repeat(tfmaskChar,
196+
utf8.RuneCountInString(value))
197+
}
198+
return value
199+
}
200+
201+
func contains(s []string, e string) bool {
202+
for _, a := range s {
203+
if a == e {
204+
return true
205+
}
206+
}
207+
return false
208+
}

0 commit comments

Comments
 (0)