絮絮叨
注:这一篇文章中 Bug 定位的部分,也就是翻源码找问题的部分单独拆到了《给 Helm 提一个 PR,重温开源项目参与过程》中。同时为了让两篇文章单独成文,也就都加了一些开头结尾,部分内容重复。
话接前回《用 IaC 的方式管理 EC2 实例 - 每天5分钟玩转 GPT 编程系列(1)》,今日个咱继续来看在给真实的开源项目修复 Bug 这个场景中,GPT 能够发挥几成功力。
剧透:今天我要到 Helm 项目里找一个 Bug,然后让 GPT-4 来修复这个 Bug。
逛社区
酒足饭饱,闲来无事,打开 Helm 项目的 issues 列表,咦,这个 issue 看起来眉清目秀呀:
其实一点都不“眉清目秀”,这段 issue 描述太冗长了,没有任何排版,看起来很累。咱就看关键词吧:helm upgrade,sensitive data,error,好的,这就够了,加在一起就是说当 helm upgrade 命令执行失败的时候,可能会导致敏感数据泄露。
盘它!
领任务
尽管已经有人留言认领这个任务,不过看描述似乎他并不会马上开始编码,而是希望进一步沟通方案。好办好办,转给我吧:
倒是挺干脆,分分钟就 assign 给我了。
不过,其实,这会我并不知道怎么修复这个 bug,我也不熟悉 Helm 的源码,外加已经小半年没咋写过 Golang 了,其实心里挺慌。八层把握吧,毕竟手握屠龙刀 GPT-4,开干!
复现 Bug
要修复 Bug,第一步自然是先看能不能稳定复现。外加 issue 描述里其实我看不出来如何复现,也不是太看清楚问题现象是什么,总之,复现问题是当务之急。
让 DevChat 准备 Bug 复现材料
如果这是你第一次听说 DevChat,来,“辟邪剑谱”接住咯:
1
2
3
4
5
6
|
我发现 Helm 在执行 upgrade 的时候,如果出错,会导致 Secret 里的数据被明文打印出来,存在敏感数据泄露风险。现在我想复现这个问题,我设想的步骤是这样的:
1. 构造一个包含 Secret 的正常的 Helm Chart,Secret 里的数据是从 values.yaml 里读取的,values.yaml 中直接使用 base64 编码字符串,而不是在 secret.yaml 中使用 b64enc;
2. 然后执行 helm install,此刻应该没有问题异常;
3. 修改 values.yaml 中的 base64 字符串,让其非法(比如删除一个结尾的“=”);
4. 执行 helm upgrade 命令,看结果是不是存在敏感数据泄露。
请告诉我相关命令和操作等。
|
首先,我会概括你的需求:
- 你想要复现一个问题,该问题是在使用 Helm 进行 upgrade 操作时,如果出错,会导致 Secret 中的数据被明文打印出来,存在敏感数据泄露风险。
- 你已经设想了一些步骤,包括创建一个包含 Secret 的 Helm Chart,然后进行一些操作来触发这个问题。
下面是我为你准备的相关命令和操作:
- 创建一个包含 Secret 的 Helm Chart。首先,你需要创建一个新的 Helm Chart,可以使用以下命令:
然后,你需要在 mychart/templates 目录下创建一个新的文件 secret.yaml,并添加以下内容:
1
2
3
4
5
6
7
|
apiVersion: v1
kind: Secret
metadata:
name: mysecret
type: Opaque
data:
mykey: {{ .Values.mysecret.mykey }}
|
在 mychart/values.yaml 文件中,添加以下内容:
1
2
|
mysecret:
mykey: "SGVsbG8gd29ybGQ=" # "Hello world" 的 base64 编码
|
- 执行 helm install 命令。在你的 Helm Chart 目录(即 mychart)下,执行以下命令:
1
|
helm install myrelease .
|
此时,应该没有任何问题或异常。
- 修改 values.yaml 文件中的 base64 字符串,让其非法。你可以将 values.yaml 文件中的 mykey 值修改为一个非法的 base64 字符串,例如删除一个结尾的“=”:
1
2
|
mysecret:
mykey: "SGVsbG8gd29ybGQ" # 非法的 base64 字符串
|
- 执行 helm upgrade 命令。在你的 Helm Chart 目录下,执行以下命令:
1
|
helm upgrade myrelease .
|
此时,你应该能看到错误信息,包括你的 Secret 数据。
以上就是复现你所说问题的步骤。如果你在执行这些步骤时遇到任何问题,或者需要更多的帮助,欢迎随时向我提问。
对着 DevChat 给的命令复现 Bug
DevChat 诚不我欺啊,一顿复制粘贴敲键盘,最后我看到了这个:
注意这个错误日志里的这部分:
1
|
Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret: "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey\":\"SGVsbG8gd29ybGQ\"}……
|
这里的 mykey: SGVsbG8gd29ybGQ 看起来似乎不太敏感,不过它其实意味着假如你存了10个密码在 Secret 里,当更新的时候写错了一个,其他正确的9个会被无差别打印出来。这个日志里包含的是一个未脱敏的完整的 Secret 资源实例内容。
定位 Bug
从日志里来看,JSON 部分大概率是 K8s 相关的库返回的内容,Helm 里直接拼接到自己日志里打印出来了。我们需要找到最接近这个“K8s 相关调用”的地方,然后找到这串日志怎么来的,再加一层“脱敏”。
翻看源码,可以在 pkg/kube/client.go
源文件中找到一个 updateResource()
函数,里面有这样一段代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
func updateResource(c *Client, target *resource.Info, currentObj runtime.Object, force bool) error {
// ……
if force {
// ……
} else {
// ……
// send patch to server
c.Log("Patch %s %q in namespace %s", kind, target.Name, target.Namespace)
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {
return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind)
}
}
// ……
}
|
这里可以看到是 helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
这个方法调用返回了一个包含敏感数据的 err。如果继续看一眼 Patch 方法的定义,就能找到:
这是 k8s.io/cli-runtime
里的代码,已经离开了 Helm 的“管辖范围”。
修复 Bug
先理思路。
琢磨下怎么修复
很明显,敏感数据来自于这几行代码:
1
2
3
4
|
obj, err = helper.Patch(target.Namespace, target.Name, patchType, patch, nil)
if err != nil {
return errors.Wrapf(err, "cannot patch %q with kind %s", target.Name, kind)
}
|
我们希望尽脱敏这些数据,自然是在当前函数/方法内完成这个过程,不能把锅留给调用方(上层函数)。所以下一步就是在 if err != nil {
之后,return
之前加入一个日志过滤函数,在这个函数内实现日志脱敏。
接着又到了和 GPT 聊需求阶段了,秉承着“能聊天就不写代码”的原则,继续祭出 DevChat。
让 DevChat 写一个日志脱敏函数
1
2
3
4
5
6
7
8
9
10
11
|
请用 Golang 帮我写一个日志脱敏函数,入参是一个包含敏感数据的字符串,类似这样:
Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret: "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey\":\"SGVsbG8gd29ybGQ\"},\"kind\":\"Secret\",\"metadata\"……,\"type\":\"Opaque\"}": illegal base64 data at input byte 12
如你所见,这是一个包含了 K8s Secret 资源实例 JSON 字符串的错误日志,其中 data 部分是一个字典,里面的所有 value 都应该被脱敏,用 *** 来打印。
你需要注意以下几点:
1. 你可以先找出从 {\"apiVersion(包含)到 ,\"kind\":\"Secret\"(不包含)之间的部分,,然后去掉\,加上一个},这样就能得到一个小的 JSON 字符串类似这样:{"apiVersion":"v1","data":{省略}},进而你可以解析出内部的字典;
3. 当你解析出来这个字典后,就可以循环遍历这个字典,完成 value 的脱敏了;
4. 脱敏完成后,你需要注意先删去刚才2中额外添加到字符串结尾的一个 },然后重新将这个新的字符串拼接回去,得到脱敏后的新字符串
5. 你可以用中文回答我,但是代码中的注释一律使用英文。
|
完整代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package main
import (
"encoding/json"
"strings"
)
// DesensitizeLog is a function to desensitize sensitive data in log
func DesensitizeLog(log string) string {
start := strings.Index(log, "{\"apiVersion")
end := strings.Index(log, ",\"kind\":\"Secret\"")
if start == -1 || end == -1 {
return log
}
// Extract the JSON string and add a } at the end
jsonStr := log[start:end] + "}"
jsonStr = strings.ReplaceAll(jsonStr, "\\\"", "\"")
// Parse the JSON string into a map
var data map[string]interface{}
err := json.Unmarshal([]byte(jsonStr), &data)
if err != nil {
return log
}
// Desensitize the values in the data map
if dataMap, ok := data["data"].(map[string]interface{}); ok {
for k := range dataMap {
dataMap[k] = "***"
}
}
// Convert the map back to JSON string
newJsonStr, err := json.Marshal(data)
if err != nil {
return log
}
// Replace the original JSON string with the new one in the log
newJsonStr = []byte(strings.ReplaceAll(string(newJsonStr), "\"", "\\\""))
newLog := log[:start] + string(newJsonStr[:len(newJsonStr)-1]) + log[end:]
return newLog
}
|
看起来大差不差,不过这两行明显写错了:
1
2
|
start := strings.Index(log, "{\"apiVersion")
end := strings.Index(log, ",\"kind\":\"Secret\"")
|
如果在双引号内要表达 \"
,那得写成 \\\"
才行。这两行要么改成:
1
2
|
start := strings.Index(log, "{\\\"apiVersion")
end := strings.Index(log, ",\\\"kind\\\":\\\"Secret\\\"")
|
要么改成:
1
2
|
start := strings.Index(log, `{\"apiVersion`)
end := strings.Index(log, `,\"kind\":\"Secret\"`)
|
我们将微调后的代码插入 Helm 项目的 client.go
中,然后加几行调用逻辑,同时把函数改成小写字母开头,顺带完善下函数注释:
继续让 DevChat 补充一个 UT
- 新开一个 topic,将当前最新函数作为上下文,要求 DevChat 写一个 UT
1
2
3
4
|
请给这个函数写一个 UT,测试字符串是:
Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret: "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey1\":\"hello\", \"mykey2\":\"world\"},\"kind\":\"Secret\",\"metadata\"……,\"type\":\"Opaque\"}": illegal base64 data at input byte 12
结果中 hello 和 world 都应该被 *** 替换。但是由于字典无序,所以 mykey1 和 mykey2 的顺序不重要,因此你只需要判断 \"mykey1\":\"***\" 和 \"mykey2\":\"***\" 在结果中存在即可。
|
看起来符合预期,直接点击 “Insert Code” 插入代码到 client_test.go
的结尾
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
package kube
import (
"strings"
"testing"
)
func TestDesensitizeLog(t *testing.T) {
log := `Error: UPGRADE FAILED: cannot patch "mysecret" with kind Secret: "" is invalid: patch: Invalid value: "{\"apiVersion\":\"v1\",\"data\":{\"mykey1\":\"hello\", \"mykey2\":\"world\"},\"kind\":\"Secret\",\"metadata\"……,\"type\":\"Opaque\"}": illegal base64 data at input byte 12`
expected1 := `\"mykey1\":\"***\"`
expected2 := `\"mykey2\":\"***\"`
result := desensitizeLog(log)
if !strings.Contains(result, expected1) {
t.Errorf("Expected %s to contain %s", result, expected1)
}
if !strings.Contains(result, expected2) {
t.Errorf("Expected %s to contain %s", result, expected2)
}
}
|
继续让 DevChat 写 Commit Message
1
2
3
4
5
|
fix: add log sanitization for Secret kind in updateResource
- Added a new function `desensitizeLog` to sanitize logs by replacing sensitive data in a Secret with "***".
- Modified the `updateResource` function to use `desensitizeLog` when the kind is "Secret".
- Added a test case for the `desensitizeLog` function to ensure it works as expected.
|
由于 DevChat 目前没有直接支持 sign-of,外加这段信息需要修改,所以这时候不能直接点击 DevChat 消息框里的“Commit”按钮,我们执行 git commit -s
命令,然后将上述消息粘贴进去,最后消息长这样:
1
2
3
4
5
6
7
|
fix: add log sanitization for Secret kind in updateResource
- Added a new function `desensitizeLog` to sanitize logs by replacing sensitive data in a Secret with "***".
- Modified the `updateResource` function to use `desensitizeLog` when the kind is "Secret".
- Added a test case for the `desensitizeLog` function to ensure it works as expected.
Signed-off-by: Daniel Hu <[email protected]>
|
这个 Commit 的完整内容可以在这里看到。
提 PR
代码写完了,下一步自然是提 PR。在这一步你可以选择先用中文表达你想要写在 PR 里的内容,然后让 DevChat 帮你翻译;也可以直接用英文写,然后让 DevChat 帮你“润色”语法,这个过程就不赘述了。这是我的 PR:
关于如何给开源社区提 PR,我在另外一篇文章里有详细介绍:
打完收工
今天的五分钟超了,收工,下班。
拓展阅读
相关资源
- GitHub:https://github.com/devchat-ai
- 注册网站:https://www.devchat.ai
- 微信公众号:微信号为“devchat-ai”,名称暂为“思码逸智能编程”
- 微信群:“思码逸智能编程”公众号内菜单栏点击“微信群”即可收到群二维码
- Discord:https://discord.gg/9t3yrbBUXD