@comate/zulu 1.4.0-beta.5 → 1.4.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (24) hide show
  1. package/comate-engine/assets/skills/auto-commit/SKILL.md +2 -0
  2. package/comate-engine/assets/skills/auto-commit-sandbox-comate/SKILL.md +2 -2
  3. package/comate-engine/assets/skills/code-security/SKILL.md +110 -41
  4. package/comate-engine/assets/skills/code-security/references/credential_hosting.md +190 -28
  5. package/comate-engine/assets/skills/code-security/references/vul_analysis-go_sql_injection.md +149 -0
  6. package/comate-engine/assets/skills/code-security/references/vul_analysis-java_sql_injection.md +185 -0
  7. package/comate-engine/assets/skills/code-security/references/vul_analysis-php_sql_injection.md +147 -0
  8. package/comate-engine/assets/skills/code-security/references/vul_analysis-python_sql_injection.md +143 -0
  9. package/comate-engine/assets/skills/code-security/references/vul_repair-go_sql_injection.md +2 -2
  10. package/comate-engine/assets/skills/code-security/references/vul_repair-sca.md +225 -0
  11. package/comate-engine/assets/skills/code-security/scripts/credential_hosting.py +12 -10
  12. package/comate-engine/assets/skills/code-security/scripts/credential_open_page.py +125 -0
  13. package/comate-engine/assets/skills/code-security/scripts/credential_poll.py +12 -9
  14. package/comate-engine/assets/skills/code-security/scripts/credential_url.py +81 -0
  15. package/comate-engine/assets/skills/code-security/scripts/ducc/get_claude_session_id.sh +33 -0
  16. package/comate-engine/assets/skills/code-security/scripts/ducc/open_browser.py +191 -0
  17. package/comate-engine/assets/skills/code-security/scripts/parse_scan_result.py +99 -16
  18. package/comate-engine/assets/skills/code-security/scripts/repair_vulnerability.py +66 -13
  19. package/comate-engine/assets/skills/code-security/scripts/scan_vulnerability.py +44 -12
  20. package/comate-engine/assets/skills/create-automation/SKILL.md +3 -0
  21. package/comate-engine/assets/skills/create-subagent/SKILL.md +16 -4
  22. package/comate-engine/server.js +137 -77
  23. package/dist/bundle/index.js +3 -3
  24. package/package.json +1 -1
@@ -9,7 +9,7 @@ go sqli 漏洞修复知识
9
9
  2. 污点数据拼接到 sql 语句时,是作为 sql 语句中 select、update、delete 等子句,且无法确定子句具体信息,也是不可修复场景,不进行漏洞修复。
10
10
 
11
11
  ### 可修复场景
12
- 1. 漏洞修复必须在 **SQL 执行语句(sink 点)所在的文件和函数内**完成,不要创建新的代码文件。
12
+ 1. 漏洞修复必须在 **SQL 语句生成 或者 SQL 语句执行所在的文件和函数内以及数据流传递链路所在文件**完成,不要创建新的代码文件。
13
13
  2. 同个文件进行参数过滤时,可以在文件头定义过滤正则,如果定义已经存在,可以直接复用。
14
14
  3. 对于 where 列名(操作符)之后的 value、insert 的values、like 的 value,需要使用**预编译/参数绑定**的方式进行漏洞修复。**注意**:LIKE 子句的值虽然可以预编译,但 `%` 和 `_` 是 SQL 通配符,若用户输入包含这些字符可能导致非预期的模糊匹配。如需精确匹配,应先转义这些字符:
15
15
 
@@ -1018,4 +1018,4 @@ Beego ORM同样存在直接拼接SQL导致注入的风险。
1018
1018
  // 安全: 使用参数化查询
1019
1019
  o.Raw("SELECT * FROM users WHERE username = ?", username).QueryRow(&user)
1020
1020
  ```
1021
- ##
1021
+ ##
@@ -0,0 +1,225 @@
1
+ SCA(软件成分分析)漏洞修复知识
2
+
3
+ ## 概述
4
+
5
+ SCA 类漏洞不是源码层面的代码缺陷,而是项目依赖了存在已知漏洞(CVE / 内部安全公告)的第三方组件。修复方式是 **升级依赖包到安全版本**,一般不需要改动业务源码。本文档只覆盖 **Go 和 Java** 两种生态。
6
+
7
+ ## 核心原则
8
+
9
+ 1. **只改依赖清单文件,不要改业务源码**。SCA 修复的本质是换版本,业务代码不应被触碰。
10
+ 2. **必须升到扫描结果里标注的 "安全版本" 或更高**,不能升到更低的补丁版本,否则复测仍会命中。
11
+ 3. **保持其他依赖不变**,只修改受影响的那一行,避免连锁破坏构建。
12
+ 4. **版本号格式必须符合该包管理器的约定**(Go 的 `v` 前缀、Maven 的 `<version>` 等),不要随意改变版本声明风格。
13
+
14
+ ## 从 `scan_result.json` 提取修复信息
15
+
16
+ 对于 SCA 类漏洞,`scan_result.json`(或解析出的 `parsed_result.json`)里通常包含以下关键字段(位置见 SARIF 的 `results[].properties` 或 `rules[]`):
17
+
18
+ | 字段 | 用途 |
19
+ |------|------|
20
+ | `importPath` | 漏洞组件的**引入路径 / 包名**(如 `github.com/baidubce/bce-sdk-go` 或 `org.apache.logging.log4j:log4j-core`)。如果为空字符串,说明该条结果不是 SCA 场景,应按源码类漏洞处理。 |
21
+ | `description` / `suggestion` | 通常会写明受影响版本区间和建议升级到的安全版本(例如 "v0.9.263 及之前版本存在漏洞,升级到 v0.9.264 及以上版本")。 |
22
+ | `file` / `startLine` | 指向依赖声明所在的文件和行,一般就是 `go.mod`、`pom.xml`、`build.gradle` 等。 |
23
+
24
+ **提取步骤**:
25
+ 1. 读取漏洞条目,取 `importPath` 作为待升级组件标识。
26
+ 2. 从 `description`、`suggestion`、`message` 中解析出安全版本号(关键词:安全版本 / 修复版本 / fixed in / upgrade to / >=)。若文本里同时出现"受影响版本上限"和"建议版本",以**建议/安全版本**为准。
27
+ 3. 定位 `file` 指向的依赖清单文件中对应 `importPath` 的那一行。
28
+
29
+ ## 修复模式
30
+
31
+ ### Go (`go.mod`)
32
+
33
+ #### 情况 1:漏洞组件是直接依赖,`go.mod` 里有对应行
34
+
35
+ **修复前**:
36
+ ```go
37
+ require (
38
+ github.com/baidubce/bce-sdk-go v0.9.263
39
+ github.com/gin-gonic/gin v1.9.1
40
+ )
41
+ ```
42
+
43
+ **修复后**(升级到安全版本 `v0.9.264` 或更高):
44
+ ```go
45
+ require (
46
+ github.com/baidubce/bce-sdk-go v0.9.266
47
+ github.com/gin-gonic/gin v1.9.1
48
+ )
49
+ ```
50
+
51
+ 要点:
52
+ - 只替换匹配 `importPath` 的那一行,保留空行/缩进/其他依赖顺序。
53
+ - `v` 前缀不能省略。
54
+ - 如果该组件同时出现在 `require (...)` 块和 `// indirect` 块里,两处都要改成同一版本。
55
+ - `go.sum` 会在构建 / `go mod tidy` 时自动刷新,**本次修复不需要手动修改 `go.sum`**。
56
+
57
+ #### 情况 2:漏洞组件是间接依赖,`go.mod` 里没有对应行
58
+
59
+ 出现条件:
60
+ - 漏洞组件 C 没被你的源码直接 `import`;
61
+ - `go.mod` 里也没有 `C ... // indirect`(因为上游直接依赖 B 的 `go.mod` 递归就能推导出 C 的当前版本,Go 不会重复写);
62
+ - 又不能/不想升级 B(B 尚未发布含安全 C 的新版,或升 B 会引入破坏性变更)。
63
+
64
+ 此时通过**新增一条 `// indirect`** 把 C 钉到安全版本。推荐用命令,不手改:
65
+
66
+ ```bash
67
+ go get github.com/xxx/C@v1.3.0 # v1.3.0 为安全版本
68
+ go mod tidy # 自动补 // indirect 注释、刷新 go.sum
69
+ ```
70
+
71
+ 效果等同于 `go.mod` 多出:
72
+
73
+ ```go
74
+ require (
75
+ github.com/xxx/C v1.3.0 // indirect
76
+ )
77
+ ```
78
+
79
+ 原理:MVS(Minimum Version Selection)取依赖图中该模块被要求过的**最大版本**。你手动声明的 `v1.3.0` 参与这次选择,且优先级不低于上游,于是最终选中 ≥ 安全版本的 C。
80
+
81
+ 必须核查的前置条件:
82
+ 1. **同一个 major 版本**。`v2+` 在 Go modules 中是**不同的模块路径**(`github.com/xxx/C/v2`),不能靠 MVS 自动升上去。若安全版本跨了 major,只能换上游依赖或自己 fork。
83
+ 2. **上游没有更高下限**。如果 B 的 `go.mod` 里写的是 `require C v1.4.0`,你钉 `v1.3.0` 无效,MVS 会选 `v1.4.0`;此时要钉到 `≥ v1.4.0 且 ≥ 安全版本`。
84
+ 3. **钉版本必须与 B 依赖的 C 保持 API 兼容**(同 major 号按语义化版本应兼容)。不兼容时上层代码会编译失败,需要改 B 或放弃此方案。
85
+
86
+ 修复后必须验证:
87
+
88
+ ```bash
89
+ go mod why github.com/xxx/C # 确认 C 仍在依赖图里
90
+ go list -m github.com/xxx/C # 打印最终选中版本,必须 ≥ 安全版本
91
+ go build ./... # 构建通过
92
+ ```
93
+
94
+ 注意事项:
95
+ - 间接依赖钉版本是**临时方案**,会带来后续维护成本(上游 B 升级时可能冲突)。长期建议推动 B 升级或自行 fork。
96
+ - 不要只在 `go.mod` 手写一行 `// indirect` 而不跑 `go mod tidy`,这会让 `go.sum` 缺失校验信息,CI 构建会失败。
97
+
98
+ ### Java (Maven `pom.xml` / Gradle `build.gradle`)
99
+
100
+ Java 的 `importPath` 通常形如 `groupId:artifactId`,例如 `org.apache.logging.log4j:log4j-core`。
101
+
102
+ #### 情况 1:漏洞组件是直接依赖
103
+
104
+ **Maven**:
105
+ ```xml
106
+ <dependency>
107
+ <groupId>org.apache.logging.log4j</groupId>
108
+ <artifactId>log4j-core</artifactId>
109
+ <version>2.17.1</version>
110
+ </dependency>
111
+ ```
112
+ 只替换 `<version>` 标签内的版本号。如果版本号通过 `<properties>` 变量引用(`${log4j.version}`),则改 `<properties>` 里的对应条目,不要在使用处硬改。
113
+
114
+ **Gradle**:
115
+ ```groovy
116
+ implementation "org.apache.logging.log4j:log4j-core:2.17.1"
117
+ ```
118
+ 替换冒号后第三段版本号即可。若版本通过 `ext` 变量定义,则改 `ext` 源头。
119
+
120
+ #### 情况 2:漏洞组件是间接依赖(由某个直接依赖 B 传递引入)
121
+
122
+ 与 Go 的 MVS 不同,**Maven 默认使用"最近原则"(nearest-wins)**:依赖树中路径最短的声明优先。因此单纯在 `pom.xml` 里加一条高版本 `<dependency>` 通常可以覆盖掉 B 带来的旧版,但更稳妥、更符合主流实践的做法是 **先 `<exclusions>` 排除掉 B 里的旧版,再显式引入新版本**。这样依赖图干净,不会出现同一个 jar 被两次引入导致的 warning,也能抵御"路径变化导致覆盖失效"的问题。
123
+
124
+ ##### Maven:排除 + 引入新版本
125
+
126
+ 漏洞组件:`com.fasterxml.jackson.core:jackson-databind`,由直接依赖 `B` 间接引入,安全版本 `2.15.4`。
127
+
128
+ 修复前(只有对 B 的声明,jackson-databind 间接引入的是旧版):
129
+ ```xml
130
+ <dependencies>
131
+ <dependency>
132
+ <groupId>com.example</groupId>
133
+ <artifactId>B</artifactId>
134
+ <version>1.0.0</version>
135
+ </dependency>
136
+ </dependencies>
137
+ ```
138
+
139
+ 修复后:
140
+ ```xml
141
+ <dependencies>
142
+ <dependency>
143
+ <groupId>com.example</groupId>
144
+ <artifactId>B</artifactId>
145
+ <version>1.0.0</version>
146
+ <!-- 从 B 的依赖传递里排除旧版 jackson-databind -->
147
+ <exclusions>
148
+ <exclusion>
149
+ <groupId>com.fasterxml.jackson.core</groupId>
150
+ <artifactId>jackson-databind</artifactId>
151
+ </exclusion>
152
+ </exclusions>
153
+ </dependency>
154
+
155
+ <!-- 显式引入安全版本 -->
156
+ <dependency>
157
+ <groupId>com.fasterxml.jackson.core</groupId>
158
+ <artifactId>jackson-databind</artifactId>
159
+ <version>2.15.4</version>
160
+ </dependency>
161
+ </dependencies>
162
+ ```
163
+
164
+ 要点:
165
+ - `<exclusions>` 放在**传递来源 B** 的 `<dependency>` 内部,不是放在 jackson-databind 自身那条。
166
+ - `<exclusion>` 里**只写 `groupId` 和 `artifactId`,不写 `<version>`**(Maven 语法要求)。
167
+ - 如果漏洞组件由多条路径同时传递进来(多个直接依赖都间接用了它),需要在**每一个**直接依赖的 `<dependency>` 里都加 `<exclusions>`,否则任何一条没排除的路径都可能拉回旧版。
168
+ - 排除之后务必新增一条显式的 `<dependency>` 指向安全版本,否则应用会因缺少 jackson-databind 编译/运行失败。
169
+ - 使用 `dependencyManagement` 统一管理版本的项目,可以把安全版本加到 `<dependencyManagement>` 里,然后 `<dependency>` 不写版本;但 `<exclusions>` 仍然要写在 B 的声明里。
170
+
171
+ ##### Gradle:排除 + 引入新版本
172
+
173
+ ```groovy
174
+ dependencies {
175
+ implementation("com.example:B:1.0.0") {
176
+ exclude group: "com.fasterxml.jackson.core", module: "jackson-databind"
177
+ }
178
+ implementation "com.fasterxml.jackson.core:jackson-databind:2.15.4"
179
+ }
180
+ ```
181
+
182
+ 要点:
183
+ - `exclude` 在 B 的 `implementation(...)` 闭包里;
184
+ - 若多个直接依赖都传递了旧版 jackson-databind,每个都要加 `exclude`;
185
+ - Gradle 也支持用 `resolutionStrategy.force` 或 `constraints` 强制版本,但排除 + 显式引入的语义更清晰,推荐作为首选。
186
+
187
+ ##### 如何确认排除是否生效
188
+
189
+ ```bash
190
+ # Maven
191
+ mvn dependency:tree -Dincludes=com.fasterxml.jackson.core:jackson-databind
192
+
193
+ # Gradle
194
+ ./gradlew dependencies --configuration runtimeClasspath | grep jackson-databind
195
+ ```
196
+
197
+ 输出中应只剩你显式声明的安全版本,且不再出现从 B 传递来的旧版行(通常行尾会打 `(*)` 或 `omitted for conflict` 标记)。
198
+
199
+ 注意事项:
200
+ - 同样需要确认安全版本与 B 内部使用 jackson-databind 的代码 **API 兼容**(Jackson 大多数 2.x 版本之间兼容,但个别版本移除了类/方法)。不兼容时要权衡改 B 或 fork。
201
+ - `<exclusions>` 是长期解决方案,比 Go 的 indirect 钉版本稳定;但同样建议推动上游 B 升级以减少维护开销。
202
+
203
+ ## 修复流程清单
204
+
205
+ 1. 从 `parsed_result.json` 的漏洞条目里拿到 `importPath`(组件名)、`file`(依赖文件)、`description` / `suggestion`(包含安全版本)。
206
+ 2. 从描述文本里解析出**安全版本**:优先匹配 "升级到 X 及以上"、"fixed in X"、"safe version X"、">= X"。
207
+ 3. Read 对应依赖文件,定位到包含 `importPath` 的那一行;找不到 → 属于间接依赖,走对应语言的"情况 2"路径。
208
+ 4. 按语言和情况执行替换/新增:
209
+ - Go 直接依赖:改 `go.mod` 里的版本号;
210
+ - Go 间接且无行:`go get pkg@安全版本 && go mod tidy`;
211
+ - Java 直接依赖:改 `<version>` / Gradle 版本串;
212
+ - Java 间接依赖:在传递来源的 `<dependency>` 里加 `<exclusions>`,并新增安全版本的 `<dependency>`。
213
+ 5. 如果该组件在同一个文件中出现多次(如 Go 的 `require` + `indirect`,Java 多个直接依赖都传递了同一个间接依赖),全部处理一致。
214
+ 6. 不要新增、删除任何其他依赖,也不要对业务源码做改动。
215
+ 7. 修复完成后继续走复测扫描流程。
216
+
217
+ ## 常见坑
218
+
219
+ - **升错方向**:安全版本通常**大于**受影响版本上限,不要把版本降到"受影响范围以下",因为旧补丁可能仍有别的 CVE。
220
+ - **Go 只改了 `require` 没改 `indirect`**:如果项目直接没依赖该组件、只在 `indirect` 中出现,建议改 `indirect` 行;或通过 `go get` 让它成为新的 indirect 条目,避免被间接依赖拉回旧版本。
221
+ - **Java 只 exclude 没引入新版本**:排除之后如果没新增安全版本的 `<dependency>`,运行时会 `ClassNotFoundException`。两步必须都做。
222
+ - **Java 只改一条路径的 exclude**:多条传递路径时,漏掉的那条会把旧版拉回来,`mvn dependency:tree` 可以定位哪些路径需要补排除。
223
+ - **锁文件/校验文件未刷新**:`go.sum`、`poetry.lock` 等在下次构建时会自动更新,修复脚本不要手动改这些文件。
224
+ - **版本号写成字符串被引用**:`pom.xml` 使用 `<properties>`、`build.gradle` 使用 `ext` 定义版本时,必须改源头定义处,不要在使用处硬改版本。
225
+ - **多个依赖文件**:monorepo 或多模块项目中,同一组件可能在多个 `go.mod` / `pom.xml` 中出现。仅修复扫描结果 `file` 指向的那一个文件即可,不要主动扩散修改。
@@ -12,19 +12,23 @@
12
12
  """
13
13
 
14
14
  import argparse
15
+ from utils import make_user_id, HOST, build_headers
15
16
  import json
16
17
  import logging
17
18
  import sys
19
+ import uuid
18
20
 
19
21
  import http_client # noqa: F401 (triggers shared logging config)
20
- import utils
22
+
23
+ USER_ID = ""
24
+ USERNAME = ""
21
25
 
22
26
  logger = logging.getLogger("credential_hosting")
23
27
 
24
28
 
25
- def hosting_credentials(chat_id, platform, credentials, username, user_id):
29
+ def hosting_credentials(chat_id, platform, credentials):
26
30
  """将凭证托管到指定平台。"""
27
- url = "{}/api/v1/deployments/{}/credentials".format(utils.HOST, platform)
31
+ url = "{}/api/v1/deployments/{}/credentials".format(HOST, platform)
28
32
  payload = {
29
33
  "configs": credentials,
30
34
  "scanChatID": chat_id,
@@ -33,14 +37,11 @@ def hosting_credentials(chat_id, platform, credentials, username, user_id):
33
37
  logger.info("credential_hosting: chat_id=%s, platform=%s, credentials_count=%d",
34
38
  chat_id, platform, len(credentials))
35
39
  print("正在托管凭证到 {} 平台...".format(platform), file=sys.stderr)
36
- return http_client.post(
37
- url,
38
- headers=utils.build_headers(username, user_id, chat_id),
39
- json_body=payload,
40
- )
40
+ return http_client.post(url, headers=build_headers(USERNAME, USER_ID, chat_id), json_body=payload)
41
41
 
42
42
 
43
43
  def main():
44
+ global USER_ID, USERNAME
44
45
  parser = argparse.ArgumentParser(description="凭证托管工具")
45
46
  parser.add_argument("--poll-result", default=None, help="credential_poll.py 输出的结果文件路径,自动提取所需参数")
46
47
  parser.add_argument("--chat-id", default=None, help="scanChatID(使用 --poll-result 时可省略)")
@@ -49,7 +50,8 @@ def main():
49
50
  parser.add_argument("--credentials", default=None, help="凭证配置 JSON(使用 --poll-result 时可省略)")
50
51
  args = parser.parse_args()
51
52
 
52
- user_id = utils.make_user_id(args.username)
53
+ USERNAME = args.username
54
+ USER_ID = make_user_id(args.username)
53
55
 
54
56
  chat_id = args.chat_id
55
57
  platform = args.platform
@@ -79,7 +81,7 @@ def main():
79
81
  print("错误: 缺少必要参数。使用 --poll-result 或同时提供 --chat-id、--platform、--credentials", file=sys.stderr)
80
82
  sys.exit(1)
81
83
 
82
- result = hosting_credentials(chat_id, platform, credentials, args.username, user_id)
84
+ result = hosting_credentials(chat_id, platform, credentials)
83
85
  print(json.dumps(result, ensure_ascii=False, indent=2))
84
86
 
85
87
 
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 打开硬编码风险治理界面 - 构建凭证配置网页 URL 并在 IDE 内嵌浏览器中打开。
4
+
5
+ 用法:
6
+ python3 credential_open_page.py --chat-id <chatID> --username <用户名> \
7
+ [--ide-name <ideType>] [--repo <repo>] [--project-dir <目录>]
8
+
9
+ 输出:
10
+ 1. 可点击的 Markdown 链接(供用户手动打开)
11
+ 2. 通过 comate-kernel socket 在 IDE 内嵌浏览器中自动打开网页
12
+ """
13
+
14
+ import argparse
15
+ import glob
16
+ import json
17
+ import os
18
+ import socket
19
+ import subprocess
20
+ import sys
21
+
22
+ # 导入公共 URL 构建模块
23
+ sys.path.insert(0, os.path.dirname(__file__))
24
+ from credential_url import build_url_from_args, build_markdown_link
25
+
26
+
27
+ def open_in_ide(url: str) -> bool:
28
+ """通过 comate-kernel Unix Socket HTTP API 在 IDE 内嵌浏览器中打开网页。
29
+
30
+ 传入 reuseExisting=True,若相同 URL 已在内嵌浏览器中打开则复用现有页签,不新建。
31
+ """
32
+ body = json.dumps({
33
+ "action": "executeVirtualEditor",
34
+ "method": "openUrlInEditorWebview",
35
+ "payload": {"url": url, "title": "凭证托管助手", "reuseExisting": True},
36
+ })
37
+ req = (
38
+ "POST /editor/command/ HTTP/1.1\r\n"
39
+ "Host: localhost\r\n"
40
+ "Content-Type: application/json\r\n"
41
+ f"Content-Length: {len(body.encode())}\r\n"
42
+ "Connection: close\r\n\r\n"
43
+ + body
44
+ )
45
+
46
+ tmpdir = os.environ.get("TMPDIR", "/tmp").rstrip("/")
47
+ ppid_sock = f"{tmpdir}/comate-kernel-{os.getppid()}.sock"
48
+ others = sorted(
49
+ set(
50
+ glob.glob(f"{tmpdir}/comate-kernel-*.sock")
51
+ + glob.glob("/tmp/comate-kernel-*.sock")
52
+ ),
53
+ key=os.path.getmtime,
54
+ reverse=True,
55
+ )
56
+ socks = ([ppid_sock] if os.path.exists(ppid_sock) else []) + [
57
+ s for s in others if s != ppid_sock
58
+ ]
59
+
60
+ for sock_path in socks:
61
+ try:
62
+ s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
63
+ s.settimeout(2)
64
+ s.connect(sock_path)
65
+ s.send(req.encode())
66
+ resp = b""
67
+ while True:
68
+ chunk = s.recv(4096)
69
+ if not chunk:
70
+ break
71
+ resp += chunk
72
+ s.close()
73
+ if b"ok" in resp:
74
+ return True
75
+ except Exception:
76
+ pass
77
+
78
+ return False
79
+
80
+
81
+ def open_via_cli(url: str) -> bool:
82
+ """降级方案:通过 IDE CLI --open-url 触发 SimpleBrowser。"""
83
+ import urllib.parse
84
+ enc = urllib.parse.quote(json.dumps([url]))
85
+ for cli in ["comate", "code"]:
86
+ try:
87
+ subprocess.run(
88
+ [cli, "--open-url", f"command:simpleBrowser.api.open?{enc}"],
89
+ timeout=3,
90
+ capture_output=True,
91
+ )
92
+ return True
93
+ except Exception:
94
+ pass
95
+ return False
96
+
97
+
98
+ def main():
99
+ """入口函数:解析参数、构建 URL、输出链接并尝试在 IDE 内嵌浏览器中打开。"""
100
+ parser = argparse.ArgumentParser(description="打开硬编码风险治理界面")
101
+ parser.add_argument("--chat-id", required=True, help="会话 ID (_CHAT_ID)")
102
+ parser.add_argument("--username", required=True, help="用户名 (_USERNAME)")
103
+ parser.add_argument("--ide-name", default=os.environ.get("COMATE_IDE_NAME", "comate"), help="IDE 类型")
104
+ parser.add_argument("--repo", default="", help="仓库标识,留空则自动从 git remote 获取")
105
+ parser.add_argument("--project-dir", default=".", help="项目目录,用于自动获取 repo")
106
+ args = parser.parse_args()
107
+
108
+ url = build_url_from_args(args.chat_id, args.username, args.ide_name,
109
+ args.project_dir, args.repo)
110
+
111
+ # 输出可点击链接(在 IDE 内嵌浏览器中打开)
112
+ print("请点击以下链接打开硬编码风险治理网页,配置凭证信息:")
113
+ print()
114
+ print(build_markdown_link(url))
115
+ print()
116
+ print("配置完成后请在网页中点击「生成代码」按钮,我会自动检测到配置完成并继续执行修复。")
117
+ print()
118
+
119
+ # 自动在 IDE 内嵌浏览器中打开
120
+ if not open_in_ide(url):
121
+ open_via_cli(url)
122
+
123
+
124
+ if __name__ == "__main__":
125
+ main()
@@ -14,7 +14,7 @@
14
14
  """
15
15
 
16
16
  import argparse
17
- import base64
17
+ from utils import make_user_id, WS_HOST
18
18
  import json
19
19
  import logging
20
20
  import os
@@ -23,15 +23,16 @@ import struct
23
23
  import sys
24
24
  import time
25
25
 
26
+ # 触发共享日志配置
27
+ import http_client # noqa: F401
28
+
26
29
  try:
27
30
  import ssl
28
31
  _SSL_AVAILABLE = True
29
32
  except ImportError:
30
33
  _SSL_AVAILABLE = False
31
34
 
32
- # 触发共享日志配置
33
- import http_client # noqa: F401
34
- import utils
35
+ USER_ID = ""
35
36
 
36
37
  logger = logging.getLogger("credential_poll")
37
38
 
@@ -49,6 +50,7 @@ OP_PONG = 0xA
49
50
 
50
51
  def _generate_ws_key():
51
52
  """生成随机的 Sec-WebSocket-Key。"""
53
+ import base64
52
54
  return base64.b64encode(os.urandom(16)).decode("ascii")
53
55
 
54
56
 
@@ -221,15 +223,15 @@ def _parse_ws_url(url):
221
223
  return host, port, path, use_ssl
222
224
 
223
225
 
224
- def poll_credential_config(chat_id, user_id):
226
+ def poll_credential_config(chat_id):
225
227
  """通过 WebSocket 监听凭证配置,返回配置数据 dict 或 None。"""
226
- url = "{}/api/v1/ws/credential/events".format(utils.WS_HOST)
228
+ url = "{}/api/v1/ws/credential/events".format(WS_HOST)
227
229
  host, port, path, use_ssl = _parse_ws_url(url)
228
230
  logger.info("credential_poll start: chat_id=%s", chat_id)
229
231
 
230
232
  ws_headers = {
231
233
  "SAST-Chat-ID": chat_id,
232
- "Comate-User-Id": user_id,
234
+ "Comate-User-Id": USER_ID,
233
235
  }
234
236
 
235
237
  reconnect_attempts = 0
@@ -318,15 +320,16 @@ def poll_credential_config(chat_id, user_id):
318
320
 
319
321
 
320
322
  def main():
323
+ global USER_ID
321
324
  parser = argparse.ArgumentParser(description="凭证配置轮询工具")
322
325
  parser.add_argument("--chat-id", required=True, help="scanChatID,用于关联扫描会话")
323
326
  parser.add_argument("--username", required=True, help="Comate 用户名")
324
327
  parser.add_argument("--output", default=None, help="将结果保存到指定文件路径(同时仍输出到标准输出)")
325
328
  args = parser.parse_args()
326
329
 
327
- user_id = utils.make_user_id(args.username)
330
+ USER_ID = make_user_id(args.username)
328
331
 
329
- result = poll_credential_config(args.chat_id, user_id)
332
+ result = poll_credential_config(args.chat_id)
330
333
  if result:
331
334
  output_json = json.dumps(result, ensure_ascii=False, indent=2)
332
335
  print(output_json)
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ 凭证配置网页 URL 构建工具(公共模块)
4
+
5
+ 被 credential_open_page.py 和 ducc/open_browser.py 共同使用。
6
+ """
7
+
8
+ import hashlib
9
+ import os
10
+ import subprocess
11
+ import urllib.parse
12
+ import json
13
+
14
+ from utils import HOST
15
+
16
+ BASE_URL = f"{HOST}/app/credential"
17
+ VERSION = "2.9.1"
18
+
19
+
20
+ def compute_uid(username: str) -> str:
21
+ """计算用户 UID:username 的 MD5 前 12 位。"""
22
+ return hashlib.md5(username.encode()).hexdigest()[:12]
23
+
24
+
25
+ def get_repo(project_dir: str) -> str:
26
+ """从 git remote 获取仓库三级路径标识,失败则回退到目录名。
27
+
28
+ SSH 格式(git@icode.baidu.com:baidu/scan/cnap-test.git):取 : 后的部分,去掉 .git 后缀。
29
+ HTTP 格式(https://icode.baidu.com/baidu/scan/cnap-test):取域名后的路径,去掉 .git 后缀。
30
+ """
31
+ try:
32
+ result = subprocess.run(
33
+ ["git", "remote", "get-url", "origin"],
34
+ cwd=project_dir,
35
+ capture_output=True,
36
+ text=True,
37
+ timeout=5,
38
+ )
39
+ remote = result.stdout.strip()
40
+ if remote:
41
+ if ":" in remote and not remote.startswith("http"):
42
+ # SSH 格式
43
+ remote = remote.split(":", 1)[1]
44
+ else:
45
+ # HTTP 格式
46
+ from urllib.parse import urlparse
47
+ remote = urlparse(remote).path.lstrip("/")
48
+ return remote.removesuffix(".git")
49
+ except Exception:
50
+ pass
51
+ return os.path.basename(os.path.abspath(project_dir))
52
+
53
+
54
+ def build_url(chat_id: str, uid: str, ide_name: str, repo: str) -> str:
55
+ """构建完整的凭证配置网页 URL(参数中的特殊字符均已 URL 编码)。"""
56
+ params = urllib.parse.urlencode({
57
+ "chatID": chat_id,
58
+ "comateUID": uid,
59
+ "version": VERSION,
60
+ "repo": repo,
61
+ "ideType": ide_name,
62
+ })
63
+ return f"{BASE_URL}?{params}"
64
+
65
+
66
+ def build_markdown_link(url: str) -> str:
67
+ """构建可点击的 Markdown 链接,点击后在 IDE SimpleBrowser 中打开。
68
+
69
+ 对 URL 做完整特殊字符转码(safe=''),确保 &、=、/、()、# 等字符
70
+ 不破坏 Markdown 链接语法或 command 参数解析。
71
+ """
72
+ enc = urllib.parse.quote(json.dumps([url]), safe="")
73
+ return f"[打开硬编码风险治理界面](command:simpleBrowser.api.open?{enc})"
74
+
75
+
76
+ def build_url_from_args(chat_id: str, username: str, ide_name: str,
77
+ project_dir: str, repo: str = "") -> str:
78
+ """从原始参数一步构建 URL(便捷方法)。"""
79
+ uid = compute_uid(username)
80
+ repo = repo or get_repo(project_dir)
81
+ return build_url(chat_id, uid, ide_name, repo)
@@ -0,0 +1,33 @@
1
+ #!/bin/bash
2
+ # 获取当前 Claude Code 会话 ID
3
+
4
+ # 获取当前工作目录的绝对路径
5
+ CURRENT_DIR=$(pwd)
6
+
7
+ # 将路径中的斜杠替换为连字符,构建项目目录名
8
+ PROJECT_DIR_NAME=$(echo "$CURRENT_DIR" | sed 's/\//-/g')
9
+
10
+ # Claude projects 目录路径
11
+ PROJECTS_DIR="$HOME/.claude/projects"
12
+ PROJECT_PATH="$PROJECTS_DIR/$PROJECT_DIR_NAME"
13
+
14
+ # 检查项目目录是否存在
15
+ if [ ! -d "$PROJECT_PATH" ]; then
16
+ echo "Error: Project directory not found: $PROJECT_PATH"
17
+ exit 1
18
+ fi
19
+
20
+ # 找到最新的 .jsonl 文件(按修改时间排序)
21
+ LATEST_JSONL=$(ls -t "$PROJECT_PATH"/*.jsonl 2>/dev/null | head -1)
22
+
23
+ # 检查是否找到文件
24
+ if [ -z "$LATEST_JSONL" ]; then
25
+ echo "Error: No session JSONL file found in $PROJECT_PATH"
26
+ exit 1
27
+ fi
28
+
29
+ # 从文件名中提取会话 ID(去掉路径和 .jsonl 后缀)
30
+ SESSION_ID=$(basename "$LATEST_JSONL" .jsonl)
31
+
32
+ # 输出会话 ID
33
+ echo "$SESSION_ID"