@comate/zulu 1.2.1-beta.2 → 1.3.1
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.
- package/comate-engine/assets/skills/auto-commit-comate/SKILL.md +260 -0
- package/comate-engine/assets/skills/auto-commit-comate/references/data_structures.md +189 -0
- package/comate-engine/assets/skills/auto-commit-comate/references/new_version_instruction.md +209 -0
- package/comate-engine/assets/skills/auto-commit-comate/references/old_version_instruction.md +208 -0
- package/comate-engine/assets/skills/auto-commit-comate/scripts/git_diff_cli.py +196 -0
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/git_utils.py +20 -10
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/client.py +69 -40
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/farseer.py +8 -9
- package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/matching.py +65 -9
- package/comate-engine/assets/skills/auto-commit-comate/scripts/match_card_cli.py +37 -0
- package/comate-engine/assets/skills/cnap-comate/SKILL.md +157 -0
- package/comate-engine/assets/skills/cnap-comate/references/cases.md +198 -0
- package/comate-engine/assets/skills/cnap-comate/references/deploy-troubleshoot.md +15 -0
- package/comate-engine/assets/skills/cnap-comate/references/install.md +43 -0
- package/comate-engine/assets/skills/cnap-comate/references/kubectl.md +55 -0
- package/comate-engine/assets/skills/cnap-comate/references/login.md +125 -0
- package/comate-engine/assets/skills/cnap-comate/references/oncall.md +24 -0
- package/comate-engine/assets/skills/cnap-comate/scripts/install_cnap_cli.sh +36 -0
- package/comate-engine/assets/skills/code-security/SKILL.md +176 -0
- package/comate-engine/assets/skills/code-security/references/credential_hosting.md +102 -0
- package/comate-engine/assets/skills/code-security/references/vul_repair_sensitive.md +219 -0
- package/comate-engine/assets/skills/code-security/scripts/build_repair_info.py +0 -0
- package/comate-engine/assets/skills/code-security/scripts/credential_hosting.py +99 -0
- package/comate-engine/assets/skills/code-security/scripts/credential_poll.py +350 -0
- package/comate-engine/assets/skills/code-security/scripts/http_client.py +173 -0
- package/comate-engine/assets/skills/code-security/scripts/parse_scan_result.py +301 -0
- package/comate-engine/assets/skills/code-security/scripts/repair_vulnerability.py +261 -0
- package/comate-engine/assets/skills/code-security/scripts/report_chat.py +198 -0
- package/comate-engine/assets/skills/code-security/scripts/scan_vulnerability.py +316 -0
- package/comate-engine/assets/skills/code-security-comate/SKILL.md +219 -0
- package/comate-engine/assets/skills/code-security-comate/references/credential_hosting.md +102 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-go_sql_injection.md +399 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-java_sql_injection.md +591 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-php_sql_injection.md +318 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair-python_sql_injection.md +198 -0
- package/comate-engine/assets/skills/code-security-comate/references/vul_repair_sensitive.md +219 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/credential_hosting.py +87 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/credential_poll.py +345 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/http_client.py +173 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/parse_scan_result.py +392 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/repair_vulnerability.py +245 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/report_chat.py +145 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/scan_vulnerability.py +444 -0
- package/comate-engine/assets/skills/code-security-comate/scripts/utils.py +153 -0
- package/comate-engine/assets/skills/comate-docs-comate/SKILL.md +148 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/doc-map-extended.md +78 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/models-and-billing.md +51 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/product-overview.md +73 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/query_content.md +83 -0
- package/comate-engine/assets/skills/comate-docs-comate/references/query_repo.md +57 -0
- package/comate-engine/assets/skills/comate-docs-comate/scripts/ku_operator.py +1575 -0
- package/comate-engine/assets/skills/create-image-comate/SKILL.md +278 -0
- package/comate-engine/assets/skills/create-skill-comate/SKILL.md +308 -217
- package/comate-engine/assets/skills/create-skill-comate/agents/analyzer.md +274 -0
- package/comate-engine/assets/skills/create-skill-comate/agents/comparator.md +202 -0
- package/comate-engine/assets/skills/create-skill-comate/agents/grader.md +223 -0
- package/comate-engine/assets/skills/create-skill-comate/assets/eval_review.html +146 -0
- package/comate-engine/assets/skills/create-skill-comate/eval-viewer/generate_review.py +489 -0
- package/comate-engine/assets/skills/create-skill-comate/eval-viewer/viewer.html +1325 -0
- package/comate-engine/assets/skills/create-skill-comate/references/schemas.md +430 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/__init__.py +0 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/__pycache__/__init__.cpython-311.pyc +0 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/__pycache__/aggregate_benchmark.cpython-311.pyc +0 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/aggregate_benchmark.py +412 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/generate_report.py +334 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/package_skill.py +140 -0
- package/comate-engine/assets/skills/create-skill-comate/scripts/utils.py +53 -0
- package/comate-engine/assets/skills/find-skills-comate/SKILL.md +15 -12
- package/comate-engine/assets/skills/find-skills-comate/scripts/fetch_skills.py +32 -3
- package/comate-engine/assets/skills/get-ugate-token-comate/SKILL.md +159 -0
- package/comate-engine/assets/skills/get-ugate-token-comate/getUgateToken.py +150 -0
- package/comate-engine/assets/skills/icafe-comate/SKILL.md +240 -0
- package/comate-engine/assets/skills/icafe-comate/references/ai-workflows.md +233 -0
- package/comate-engine/assets/skills/icafe-comate/references/commands.md +1147 -0
- package/comate-engine/assets/skills/icafe-comate/references/error-handling.md +164 -0
- package/comate-engine/assets/skills/icafe-comate/references/git-auto-bindcard-workflow.md +201 -0
- package/comate-engine/assets/skills/icafe-comate/references/git-bindcard-workflow.md +327 -0
- package/comate-engine/assets/skills/icafe-comate/references/iql-syntax.md +327 -0
- package/comate-engine/assets/skills/icafe-comate/references/platform-concepts.md +317 -0
- package/comate-engine/assets/skills/icafe-comate/references/smart-create-workflow.md +171 -0
- package/comate-engine/assets/skills/icafe-comate/references/smart-find-workflow.md +127 -0
- package/comate-engine/assets/skills/icafe-comate/references/smart-update-workflow.md +118 -0
- package/comate-engine/assets/skills/icode-comate/SKILL.md +366 -0
- package/comate-engine/assets/skills/icode-comate/references/api/add_reviewers.md +44 -0
- package/comate-engine/assets/skills/icode-comate/references/api/build_fetch_command.md +89 -0
- package/comate-engine/assets/skills/icode-comate/references/api/check_repo_permission.md +89 -0
- package/comate-engine/assets/skills/icode-comate/references/api/create_branch.md +79 -0
- package/comate-engine/assets/skills/icode-comate/references/api/create_draft_comment.md +109 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_ai_cr_result.md +190 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_ai_review.md +97 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_diff_content.md +92 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_diff_file.md +88 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_machine_check.md +73 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_my_reviews.md +115 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_person_commit.md +89 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_person_repo.md +63 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_branch.md +62 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_config.md +91 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_members.md +118 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_repo_reviews.md +91 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_review_comments.md +87 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_review_info.md +81 -0
- package/comate-engine/assets/skills/icode-comate/references/api/get_submit_settings.md +105 -0
- package/comate-engine/assets/skills/icode-comate/references/api/icode-api.md +86 -0
- package/comate-engine/assets/skills/icode-comate/references/api/publish_comments.md +72 -0
- package/comate-engine/assets/skills/icode-comate/references/api/set_review_score.md +58 -0
- package/comate-engine/assets/skills/icode-comate/references/api/start_ai_review.md +77 -0
- package/comate-engine/assets/skills/icode-comate/references/api/submit_review.md +50 -0
- package/comate-engine/assets/skills/icode-comate/references/api/trigger_ai_cr.md +63 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/add-reviewer.md +92 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/fix-machine-check.md +144 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/merge-cr.md +100 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/ssh-setup.md +106 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/submit-acr.md +135 -0
- package/comate-engine/assets/skills/icode-comate/references/feature/submit-cr.md +123 -0
- package/comate-engine/assets/skills/icode-comate/references/git/clone.md +67 -0
- package/comate-engine/assets/skills/icode-comate/references/git/icode-git.md +68 -0
- package/comate-engine/assets/skills/icode-comate/references/git/push.md +64 -0
- package/comate-engine/assets/skills/icode-comate/references/git/push_cr.md +103 -0
- package/comate-engine/assets/skills/icode-comate/references/install.md +144 -0
- package/comate-engine/assets/skills/icode-comate/references/login.md +111 -0
- package/comate-engine/assets/skills/icode-comate/scripts/add-reviewer.sh +154 -0
- package/comate-engine/assets/skills/icode-comate/scripts/common.sh +145 -0
- package/comate-engine/assets/skills/icode-comate/scripts/fix-machine-check.sh +131 -0
- package/comate-engine/assets/skills/icode-comate/scripts/merge-cr.sh +105 -0
- package/comate-engine/assets/skills/icode-comate/scripts/ssh-setup.sh +159 -0
- package/comate-engine/assets/skills/icode-comate/scripts/submit-acr.sh +236 -0
- package/comate-engine/assets/skills/icode-comate/scripts/submit-cr.sh +104 -0
- package/comate-engine/assets/skills/icode-comate/scripts/test-preflight.sh +89 -0
- package/comate-engine/assets/skills/ku-operator-comate/SKILL.md +121 -0
- package/comate-engine/assets/skills/ku-operator-comate/examples.md +190 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/add_member.md +49 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/change_scope.md +38 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/copy_doc.md +50 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/create_doc.md +61 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/delete_doc.md +31 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/edit_content.md +568 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/move_doc.md +45 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_comment.md +79 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_content.md +83 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_flowchart.md +84 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_permission.md +38 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_recent_view.md +67 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_repo.md +57 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/query_user_info.md +37 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/update_member.md +41 -0
- package/comate-engine/assets/skills/ku-operator-comate/references/upload_attachment.md +52 -0
- package/comate-engine/assets/skills/ku-operator-comate/scripts/ku_operator.py +1575 -0
- package/comate-engine/node_modules/better-sqlite3/node_modules/.bin/prebuild-install +2 -2
- package/comate-engine/node_modules/tree-sitter-bash/node_modules/.bin/node-gyp-build +2 -2
- package/comate-engine/node_modules/tree-sitter-bash/node_modules/.bin/node-gyp-build-optional +2 -2
- package/comate-engine/node_modules/tree-sitter-bash/node_modules/.bin/node-gyp-build-test +2 -2
- package/comate-engine/package.json +2 -0
- package/comate-engine/server.js +170 -46
- package/dist/bundle/index.js +8 -8
- package/package.json +1 -1
- package/comate-engine/assets/skills/figma2code-comate/codeConnect.md +0 -37
- package/comate-engine/assets/skills/figma2code-comate/designToken.md +0 -3
- package/comate-engine/assets/skills/figma2code-comate/f2cMcp.md +0 -59
- package/comate-engine/assets/skills/smart-commit/SKILL.md +0 -646
- package/comate-engine/node_modules/@comate/plugin-host/dist/index-AZIho4HV.js +0 -1
- package/comate-engine/node_modules/@comate/plugin-host/dist/user-BIpzRUfb.js +0 -44
- package/comate-engine/node_modules/better-sqlite3/build/Release/better_sqlite3.node +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/references/issue_type_mapping.json +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/references/query_reference.md +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/compat.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/create_card_cli.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/icafe/__init__.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/logger.py +0 -0
- /package/comate-engine/assets/skills/{smart-commit → auto-commit-comate}/scripts/recognize_card_cli.py +0 -0
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
漏洞扫描工具 - 将项目代码上传至 Comate 安全服务端进行 SAST 扫描。
|
|
4
|
+
|
|
5
|
+
用法:
|
|
6
|
+
python3 scan_vulnerability.py --root-path <项目目录> --username <用户名>
|
|
7
|
+
|
|
8
|
+
流程:
|
|
9
|
+
1. 获取扫描配置(支持的文件类型)
|
|
10
|
+
2. 遍历项目目录,收集文件哈希
|
|
11
|
+
3. 创建 bundle 并上传缺失文件
|
|
12
|
+
4. 发起扫描并轮询结果
|
|
13
|
+
5. 输出 SARIF 格式漏洞报告
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
import http_client # noqa: F401 (triggers shared logging config)
|
|
25
|
+
import utils
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger("scan")
|
|
28
|
+
|
|
29
|
+
# 扫描轮询最大次数(每次间隔 3 秒,约 15 分钟)
|
|
30
|
+
MAX_SCAN_POLLS = 300
|
|
31
|
+
SCAN_POLL_INTERVAL = 3
|
|
32
|
+
# AI 误报分析等待超时(秒,默认 20 分钟)
|
|
33
|
+
AI_ANALYSIS_TIMEOUT = 1200
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_gitignore(gitignore_path):
|
|
37
|
+
# type: (str) -> tuple
|
|
38
|
+
"""解析 .gitignore 文件,返回 (ignore_patterns, negation_patterns) 正则表达式元组。
|
|
39
|
+
|
|
40
|
+
支持:
|
|
41
|
+
- `**` 匹配任意多级目录
|
|
42
|
+
- `!` 取消忽略
|
|
43
|
+
- `*` / `?` 通配符
|
|
44
|
+
- `/` 开头锚定根目录
|
|
45
|
+
- `/` 结尾匹配目录
|
|
46
|
+
"""
|
|
47
|
+
ignore_patterns = []
|
|
48
|
+
negation_patterns = []
|
|
49
|
+
try:
|
|
50
|
+
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
51
|
+
for line in f:
|
|
52
|
+
line = line.strip()
|
|
53
|
+
if not line or line.startswith("#"):
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
is_negation = line.startswith("!")
|
|
57
|
+
if is_negation:
|
|
58
|
+
line = line[1:]
|
|
59
|
+
|
|
60
|
+
regex = line
|
|
61
|
+
regex = regex.replace(".", r"\.")
|
|
62
|
+
# 先处理 ** 再处理 *,避免 ** 被错误替换
|
|
63
|
+
regex = regex.replace("/**/", r"(?:/|/.+/)")
|
|
64
|
+
regex = regex.replace("**/", r"(?:|.+/)")
|
|
65
|
+
regex = regex.replace("/**", r"(?:|.*)")
|
|
66
|
+
regex = regex.replace("**", r".*")
|
|
67
|
+
regex = regex.replace("*", "[^/]*")
|
|
68
|
+
regex = regex.replace("?", "[^/]")
|
|
69
|
+
|
|
70
|
+
if line.startswith("/"):
|
|
71
|
+
regex = "^" + regex[1:]
|
|
72
|
+
if line.endswith("/"):
|
|
73
|
+
regex = regex[:-1] + r"($|/.*)"
|
|
74
|
+
|
|
75
|
+
compiled = re.compile(regex)
|
|
76
|
+
if is_negation:
|
|
77
|
+
negation_patterns.append(compiled)
|
|
78
|
+
else:
|
|
79
|
+
ignore_patterns.append(compiled)
|
|
80
|
+
except FileNotFoundError:
|
|
81
|
+
pass
|
|
82
|
+
return ignore_patterns, negation_patterns
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def is_ignored(path, gitignore_patterns, root_dir):
|
|
86
|
+
# type: (str, tuple, str) -> bool
|
|
87
|
+
rel_path = os.path.relpath(path, root_dir)
|
|
88
|
+
ignore_patterns, negation_patterns = gitignore_patterns
|
|
89
|
+
# 先检查是否被忽略
|
|
90
|
+
matched = any(p.search(rel_path) for p in ignore_patterns)
|
|
91
|
+
if not matched:
|
|
92
|
+
return False
|
|
93
|
+
# 再检查是否被取消忽略
|
|
94
|
+
if any(p.search(rel_path) for p in negation_patterns):
|
|
95
|
+
return False
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def walk_dir(directory, extensions=None, gitignore_patterns=None):
|
|
100
|
+
# type: (str, list | None, list | None) -> list
|
|
101
|
+
"""递归遍历目录,按扩展名过滤并排除 .gitignore 中的文件。"""
|
|
102
|
+
files = []
|
|
103
|
+
resolved_dir = os.path.realpath(directory)
|
|
104
|
+
for root, dirs, filenames in os.walk(resolved_dir):
|
|
105
|
+
# 跳过常见大目录
|
|
106
|
+
dirs[:] = [d for d in dirs if d not in utils.SKIP_DIRS]
|
|
107
|
+
|
|
108
|
+
if gitignore_patterns:
|
|
109
|
+
dirs[:] = [
|
|
110
|
+
d for d in dirs
|
|
111
|
+
if not is_ignored(os.path.join(root, d), gitignore_patterns, resolved_dir)
|
|
112
|
+
]
|
|
113
|
+
for name in filenames:
|
|
114
|
+
full_path = os.path.join(root, name)
|
|
115
|
+
# 跳过过大的文件
|
|
116
|
+
try:
|
|
117
|
+
if os.path.getsize(full_path) > utils.MAX_FILE_SIZE:
|
|
118
|
+
continue
|
|
119
|
+
except OSError:
|
|
120
|
+
continue
|
|
121
|
+
if gitignore_patterns and is_ignored(full_path, gitignore_patterns, resolved_dir):
|
|
122
|
+
continue
|
|
123
|
+
if extensions:
|
|
124
|
+
if not any(name.lower().endswith(ext.lower()) for ext in extensions):
|
|
125
|
+
continue
|
|
126
|
+
files.append(full_path)
|
|
127
|
+
return files
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_settings(username, user_id):
|
|
131
|
+
# type: (str, str) -> dict
|
|
132
|
+
"""获取扫描配置。"""
|
|
133
|
+
return http_client.get(
|
|
134
|
+
"{}/api/v2/analysis/settings".format(utils.HOST),
|
|
135
|
+
headers=utils.build_headers(username, user_id),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def create_bundle(file_hashes, username, user_id):
|
|
140
|
+
# type: (dict, str, str) -> tuple
|
|
141
|
+
"""创建扫描 bundle,返回 (bundle_hash, missing_files)。"""
|
|
142
|
+
data = http_client.post(
|
|
143
|
+
"{}/api/v1/bundle".format(utils.HOST),
|
|
144
|
+
headers=utils.build_headers(username, user_id),
|
|
145
|
+
json_body=file_hashes,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if isinstance(data.get("data"), list):
|
|
149
|
+
return data.get("bundleHash", ""), data["data"]
|
|
150
|
+
return (
|
|
151
|
+
data.get("data", {}).get("bundleHash", ""),
|
|
152
|
+
data.get("data", {}).get("missingFiles", []),
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def upload_files(bundle_hash, root_path, missing_files, file_hashes, username, user_id, upload_type="scan"):
|
|
157
|
+
# type: (str, str, list, dict, str, str, str) -> tuple
|
|
158
|
+
"""上传缺失文件,返回 (bundle_hash, remaining_missing_files)。"""
|
|
159
|
+
payload = {"files": {}}
|
|
160
|
+
for name in missing_files:
|
|
161
|
+
file_path = os.path.join(root_path, name)
|
|
162
|
+
try:
|
|
163
|
+
content = utils.read_file_content(file_path)
|
|
164
|
+
payload["files"][name] = {"hash": file_hashes.get(name, ""), "content": content}
|
|
165
|
+
except Exception as e:
|
|
166
|
+
print("警告: 读取文件失败 {}: {}".format(file_path, e), file=sys.stderr)
|
|
167
|
+
|
|
168
|
+
if upload_type == "scan":
|
|
169
|
+
url = "{}/api/v1/bundle/{}".format(utils.HOST, bundle_hash)
|
|
170
|
+
else:
|
|
171
|
+
url = "{}/api/v1/upload".format(utils.HOST)
|
|
172
|
+
|
|
173
|
+
data = http_client.put(url, headers=utils.build_headers(username, user_id), json_body=payload)
|
|
174
|
+
|
|
175
|
+
if isinstance(data.get("data"), list):
|
|
176
|
+
return data.get("bundleHash", bundle_hash), data["data"]
|
|
177
|
+
return (
|
|
178
|
+
data.get("data", {}).get("bundleHash", bundle_hash),
|
|
179
|
+
data.get("data", {}).get("missingFiles", []),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def scan_init(root_path, username, user_id):
|
|
184
|
+
# type: (str, str, str) -> tuple
|
|
185
|
+
"""初始化扫描:获取配置、收集文件、创建 bundle、上传文件。返回 (bundle_hash, file_hashes)。"""
|
|
186
|
+
# 获取配置
|
|
187
|
+
cfg = get_settings(username, user_id)
|
|
188
|
+
scan_config = cfg.get("data", {}).get("scanConfiguration", {})
|
|
189
|
+
extensions = list(
|
|
190
|
+
set(scan_config.get("sca", {}).get("supportedLanguages", []))
|
|
191
|
+
| set(scan_config.get("sast", {}).get("supportedLanguages", []))
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# 解析 .gitignore
|
|
195
|
+
gitignore_path = os.path.join(root_path, ".gitignore")
|
|
196
|
+
gitignore_patterns = parse_gitignore(gitignore_path)
|
|
197
|
+
|
|
198
|
+
# 遍历目录,收集文件哈希
|
|
199
|
+
all_files = walk_dir(root_path, extensions, gitignore_patterns)
|
|
200
|
+
file_hashes = {} # type: dict
|
|
201
|
+
for f in all_files:
|
|
202
|
+
rel = os.path.relpath(f, root_path)
|
|
203
|
+
try:
|
|
204
|
+
file_hashes[rel] = utils.calc_sha256(f)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
print("警告: 计算哈希失败 {}: {}".format(f, e), file=sys.stderr)
|
|
207
|
+
|
|
208
|
+
print("收集文件: {} 个".format(len(file_hashes)), file=sys.stderr)
|
|
209
|
+
|
|
210
|
+
# 创建 bundle
|
|
211
|
+
bundle_hash, missing_files = create_bundle(file_hashes, username, user_id)
|
|
212
|
+
print("Bundle: {}, 待上传: {} 个文件".format(bundle_hash, len(missing_files)), file=sys.stderr)
|
|
213
|
+
|
|
214
|
+
# 循环上传缺失文件
|
|
215
|
+
upload_retry = 0
|
|
216
|
+
MAX_UPLOAD_RETRIES = 10
|
|
217
|
+
while missing_files:
|
|
218
|
+
bundle_hash, missing_files = upload_files(
|
|
219
|
+
bundle_hash, root_path, missing_files, file_hashes, username, user_id, "scan"
|
|
220
|
+
)
|
|
221
|
+
upload_retry += 1
|
|
222
|
+
if missing_files:
|
|
223
|
+
print("剩余待上传: {} 个文件 (重试 {}/{})".format(len(missing_files), upload_retry,
|
|
224
|
+
MAX_UPLOAD_RETRIES), file=sys.stderr)
|
|
225
|
+
if upload_retry >= MAX_UPLOAD_RETRIES:
|
|
226
|
+
print("错误: 上传重试 {} 次后仍有 {} 个文件未上传".format(MAX_UPLOAD_RETRIES, len(missing_files)), file=sys.stderr)
|
|
227
|
+
sys.exit(1)
|
|
228
|
+
|
|
229
|
+
return bundle_hash, file_hashes
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_analyzing_count(result):
|
|
233
|
+
# type: (dict) -> int
|
|
234
|
+
"""统计处于 AI 分析中状态的漏洞数量。aiAnalysisStatus: 0-无需分析, 1-分析中, 2-真实漏洞, 3-误报"""
|
|
235
|
+
data = result.get("data", {})
|
|
236
|
+
runs = data.get("sarif", {}).get("runs") or data.get("runs") or []
|
|
237
|
+
count = 0
|
|
238
|
+
for run in runs:
|
|
239
|
+
results = run.get("results") or run.get("result") or []
|
|
240
|
+
for r in results:
|
|
241
|
+
status = r.get("properties", {}).get("aiAnalysisStatus", 0)
|
|
242
|
+
if status == 1:
|
|
243
|
+
count += 1
|
|
244
|
+
return count
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _poll_ai_analysis(scan_info, chat_id, username, user_id, ai_analysis_timeout):
|
|
248
|
+
# type: (dict, str, str, str, int) -> dict
|
|
249
|
+
"""轮询等待 AI 分析完成,返回最新结果。"""
|
|
250
|
+
start_time = time.time()
|
|
251
|
+
poll_interval = 10
|
|
252
|
+
|
|
253
|
+
while True:
|
|
254
|
+
elapsed = time.time() - start_time
|
|
255
|
+
if elapsed >= ai_analysis_timeout:
|
|
256
|
+
print("\nAI 分析等待超时(已等待 {} 分钟),将使用当前结果继续".format(
|
|
257
|
+
int(elapsed // 60)), file=sys.stderr)
|
|
258
|
+
logger.warning("AI analysis timeout after %d seconds", int(elapsed))
|
|
259
|
+
break
|
|
260
|
+
|
|
261
|
+
time.sleep(poll_interval)
|
|
262
|
+
|
|
263
|
+
result = http_client.post(
|
|
264
|
+
"{}/api/v2/analysis".format(utils.HOST),
|
|
265
|
+
headers=utils.build_headers(username, user_id, chat_id),
|
|
266
|
+
json_body=scan_info,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
analyzing_count = get_analyzing_count(result)
|
|
270
|
+
if analyzing_count == 0:
|
|
271
|
+
print("\nAI 分析完成!", file=sys.stderr)
|
|
272
|
+
logger.info("AI analysis completed after %d seconds", int(time.time() - start_time))
|
|
273
|
+
return result
|
|
274
|
+
|
|
275
|
+
elapsed_min = int(elapsed // 60)
|
|
276
|
+
elapsed_sec = int(elapsed % 60)
|
|
277
|
+
print("AI 分析中... 剩余 {} 个漏洞待分析(已等待 {}分{}秒)".format(
|
|
278
|
+
analyzing_count, elapsed_min, elapsed_sec), file=sys.stderr)
|
|
279
|
+
|
|
280
|
+
return result
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def scan_vulnerability(root_path, chat_id="", username="", user_id="", wait_ai=True):
|
|
284
|
+
# type: (str, str, str, str, bool) -> tuple
|
|
285
|
+
"""执行漏洞扫描,返回 (SARIF 格式结果, bundleHash)。
|
|
286
|
+
|
|
287
|
+
Args:
|
|
288
|
+
root_path: 项目根目录
|
|
289
|
+
chat_id: 对话 ID
|
|
290
|
+
username: Comate 用户名
|
|
291
|
+
user_id: 用户 ID
|
|
292
|
+
wait_ai: 是否等待 AI 分析完成,False 则扫描完成后立即返回
|
|
293
|
+
"""
|
|
294
|
+
bundle_hash, file_hashes = scan_init(root_path, username, user_id)
|
|
295
|
+
|
|
296
|
+
git_info = utils.get_git_info(root_path)
|
|
297
|
+
scan_info = {
|
|
298
|
+
"key": {"hash": bundle_hash, "type": "file"},
|
|
299
|
+
"scan": 3,
|
|
300
|
+
"analysisContext": {
|
|
301
|
+
"initiator": "",
|
|
302
|
+
"trigger": "manual",
|
|
303
|
+
"workspaceName": "",
|
|
304
|
+
"workspacePath": "",
|
|
305
|
+
"gitInfo": [{
|
|
306
|
+
"path": "",
|
|
307
|
+
"gitURL": git_info["gitURL"],
|
|
308
|
+
"gitBranch": git_info["gitBranch"],
|
|
309
|
+
"gitCommitID": git_info["gitCommitID"],
|
|
310
|
+
}],
|
|
311
|
+
},
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
headers = utils.build_headers(username, user_id, chat_id)
|
|
315
|
+
|
|
316
|
+
print("开始扫描...", file=sys.stderr)
|
|
317
|
+
logger.info("scan start: chat_id=%s, bundle_hash=%s", chat_id, bundle_hash)
|
|
318
|
+
|
|
319
|
+
# 第一阶段:等待扫描完成
|
|
320
|
+
poll_count = 0
|
|
321
|
+
while poll_count < MAX_SCAN_POLLS:
|
|
322
|
+
result = http_client.post(
|
|
323
|
+
"{}/api/v2/analysis".format(utils.HOST), headers=headers, json_body=scan_info
|
|
324
|
+
)
|
|
325
|
+
if result.get("status") != 1:
|
|
326
|
+
break
|
|
327
|
+
poll_count += 1
|
|
328
|
+
print("扫描中,等待结果... ({}/{})".format(poll_count, MAX_SCAN_POLLS), file=sys.stderr)
|
|
329
|
+
time.sleep(SCAN_POLL_INTERVAL)
|
|
330
|
+
|
|
331
|
+
if result.get("status") == 1:
|
|
332
|
+
print("错误: 扫描超时,已轮询 {} 次仍未完成".format(MAX_SCAN_POLLS), file=sys.stderr)
|
|
333
|
+
return {"status": -1, "message": "扫描超时"}, bundle_hash
|
|
334
|
+
|
|
335
|
+
# 第二阶段:等待 AI 分析完成(可选)
|
|
336
|
+
analyzing_count = get_analyzing_count(result)
|
|
337
|
+
if analyzing_count > 0:
|
|
338
|
+
if wait_ai:
|
|
339
|
+
print("\n检测到 {} 个漏洞正在进行 AI 误报分析,等待分析完成...".format(analyzing_count), file=sys.stderr)
|
|
340
|
+
result = _poll_ai_analysis(scan_info, chat_id, username, user_id, AI_ANALYSIS_TIMEOUT)
|
|
341
|
+
else:
|
|
342
|
+
print("\n扫描完成,{} 个漏洞正在 AI 分析中(已跳过等待)".format(analyzing_count), file=sys.stderr)
|
|
343
|
+
|
|
344
|
+
return result, bundle_hash
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def wait_ai_analysis(root_path, scan_result_path, chat_id="", username="", user_id=""):
|
|
348
|
+
# type: (str, str, str, str, str) -> tuple
|
|
349
|
+
"""仅等待 AI 分析完成,不重新扫描。从已有 scan_result.json 恢复上下文。"""
|
|
350
|
+
with open(scan_result_path, "r", encoding="utf-8") as f:
|
|
351
|
+
result = json.load(f)
|
|
352
|
+
|
|
353
|
+
bundle_hash = result.get("bundleHash", "")
|
|
354
|
+
if not bundle_hash:
|
|
355
|
+
print("错误: scan_result.json 中未找到 bundleHash", file=sys.stderr)
|
|
356
|
+
sys.exit(1)
|
|
357
|
+
|
|
358
|
+
analyzing_count = get_analyzing_count(result)
|
|
359
|
+
if analyzing_count == 0:
|
|
360
|
+
print("所有漏洞 AI 分析已完成,无需等待", file=sys.stderr)
|
|
361
|
+
return result, bundle_hash
|
|
362
|
+
|
|
363
|
+
git_info = utils.get_git_info(root_path)
|
|
364
|
+
scan_info = {
|
|
365
|
+
"key": {"hash": bundle_hash, "type": "file"},
|
|
366
|
+
"scan": 3,
|
|
367
|
+
"analysisContext": {
|
|
368
|
+
"initiator": "",
|
|
369
|
+
"trigger": "manual",
|
|
370
|
+
"workspaceName": "",
|
|
371
|
+
"workspacePath": "",
|
|
372
|
+
"gitInfo": [{
|
|
373
|
+
"path": "",
|
|
374
|
+
"gitURL": git_info["gitURL"],
|
|
375
|
+
"gitBranch": git_info["gitBranch"],
|
|
376
|
+
"gitCommitID": git_info["gitCommitID"],
|
|
377
|
+
}],
|
|
378
|
+
},
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
print("等待 {} 个漏洞的 AI 分析完成...".format(analyzing_count), file=sys.stderr)
|
|
382
|
+
result = _poll_ai_analysis(scan_info, chat_id, username, user_id, AI_ANALYSIS_TIMEOUT)
|
|
383
|
+
result["bundleHash"] = bundle_hash
|
|
384
|
+
return result, bundle_hash
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def main():
|
|
388
|
+
parser = argparse.ArgumentParser(description="代码安全漏洞扫描工具")
|
|
389
|
+
parser.add_argument("--root-path", required=True, help="待扫描项目根目录")
|
|
390
|
+
parser.add_argument("--username", required=True, help="Comate 用户名")
|
|
391
|
+
parser.add_argument("--chat-id", default="", help="对话唯一标识 (COMATE_SESSION_ID)")
|
|
392
|
+
parser.add_argument("--output-dir", default=None, help="结果输出目录,默认为 skill 临时目录")
|
|
393
|
+
parser.add_argument("--no-wait-ai", action="store_true",
|
|
394
|
+
help="扫描完成后不等待 AI 分析,立即返回结果")
|
|
395
|
+
parser.add_argument("--wait-ai-only", action="store_true",
|
|
396
|
+
help="仅等待 AI 分析完成(需配合 --scan-result 使用)")
|
|
397
|
+
parser.add_argument("--scan-result", default=None,
|
|
398
|
+
help="已有扫描结果文件路径(与 --wait-ai-only 配合使用)")
|
|
399
|
+
args = parser.parse_args()
|
|
400
|
+
|
|
401
|
+
user_id = utils.make_user_id(args.username)
|
|
402
|
+
|
|
403
|
+
logger.info("scan_vulnerability start: chat_id=%s, username=%s, root_path=%s",
|
|
404
|
+
args.chat_id, args.username, args.root_path)
|
|
405
|
+
|
|
406
|
+
root_path = os.path.realpath(args.root_path)
|
|
407
|
+
if not os.path.isdir(root_path):
|
|
408
|
+
print("错误: 目录不存在 {}".format(root_path), file=sys.stderr)
|
|
409
|
+
sys.exit(1)
|
|
410
|
+
|
|
411
|
+
if args.wait_ai_only:
|
|
412
|
+
# --wait-ai-only 模式:仅等待 AI 分析,不重新扫描
|
|
413
|
+
if not args.scan_result:
|
|
414
|
+
print("错误: --wait-ai-only 需要配合 --scan-result 使用", file=sys.stderr)
|
|
415
|
+
sys.exit(1)
|
|
416
|
+
result, bundle_hash = wait_ai_analysis(
|
|
417
|
+
root_path, args.scan_result, chat_id=args.chat_id,
|
|
418
|
+
username=args.username, user_id=user_id,
|
|
419
|
+
)
|
|
420
|
+
# 原地更新 scan_result.json
|
|
421
|
+
output_file = os.path.realpath(args.scan_result)
|
|
422
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
423
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
424
|
+
print(output_file)
|
|
425
|
+
else:
|
|
426
|
+
# 正常扫描模式
|
|
427
|
+
output_dir = utils.get_output_dir(root_path, args.output_dir)
|
|
428
|
+
|
|
429
|
+
result, bundle_hash = scan_vulnerability(
|
|
430
|
+
root_path, chat_id=args.chat_id, username=args.username, user_id=user_id,
|
|
431
|
+
wait_ai=not args.no_wait_ai,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# 将 bundleHash 写入结果,供后续解析和修复使用
|
|
435
|
+
result["bundleHash"] = bundle_hash
|
|
436
|
+
|
|
437
|
+
output_file = os.path.join(output_dir, "scan_result.json")
|
|
438
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
439
|
+
json.dump(result, f, ensure_ascii=False, indent=2)
|
|
440
|
+
print(output_file)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
if __name__ == "__main__":
|
|
444
|
+
main()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
公共工具函数 - 各脚本共享的常量和辅助函数。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import base64
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import time
|
|
12
|
+
import uuid
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
HOST = "https://comate-sec.baidu-int.com"
|
|
16
|
+
WS_HOST = "wss://comate-sec.baidu-int.com"
|
|
17
|
+
|
|
18
|
+
# 常见应跳过的大目录(即使 .gitignore 未列出)
|
|
19
|
+
SKIP_DIRS = {".git", ".svn", ".hg", "node_modules", "__pycache__", "vendor", ".idea", ".vscode"}
|
|
20
|
+
|
|
21
|
+
# 单文件最大扫描大小(10MB)
|
|
22
|
+
MAX_FILE_SIZE = 10 * 1024 * 1024
|
|
23
|
+
|
|
24
|
+
# 临时数据过期时间(秒),超过此时间的 .tmp 子目录将被自动清理
|
|
25
|
+
TMP_EXPIRE_SECONDS = 24 * 60 * 60 # 24 小时
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def calc_sha256(file_path):
|
|
29
|
+
# type: (str) -> str
|
|
30
|
+
"""计算文件 SHA256 哈希。"""
|
|
31
|
+
sha256_hash = hashlib.sha256()
|
|
32
|
+
with open(file_path, "rb") as f:
|
|
33
|
+
for chunk in iter(lambda: f.read(4096), b""):
|
|
34
|
+
sha256_hash.update(chunk)
|
|
35
|
+
return sha256_hash.hexdigest()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def calc_sha256_safe(file_path):
|
|
39
|
+
# type: (str) -> str
|
|
40
|
+
"""计算文件 SHA256 哈希,失败返回空字符串。"""
|
|
41
|
+
try:
|
|
42
|
+
return calc_sha256(file_path)
|
|
43
|
+
except Exception:
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def read_file_content(file_path):
|
|
48
|
+
# type: (str) -> str
|
|
49
|
+
"""读取文件内容,二进制文件用 base64 编码。"""
|
|
50
|
+
try:
|
|
51
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
52
|
+
return f.read()
|
|
53
|
+
except UnicodeDecodeError:
|
|
54
|
+
with open(file_path, "rb") as f:
|
|
55
|
+
return base64.b64encode(f.read()).decode("ascii")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def make_user_id(username):
|
|
59
|
+
# type: (str) -> str
|
|
60
|
+
"""从用户名生成 USER_ID(MD5 前 12 位)。"""
|
|
61
|
+
return hashlib.md5(username.encode("utf-8")).hexdigest()[:12]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def build_headers(username, user_id, chat_id=""):
|
|
65
|
+
# type: (str, str, str) -> dict
|
|
66
|
+
"""构建统一的 HTTP 请求头。"""
|
|
67
|
+
headers = {
|
|
68
|
+
"Comate-Username": username,
|
|
69
|
+
"Comate-User-Id": user_id,
|
|
70
|
+
"SAST-Request-ID": str(uuid.uuid4()),
|
|
71
|
+
}
|
|
72
|
+
if chat_id:
|
|
73
|
+
headers["SAST-Chat-ID"] = chat_id
|
|
74
|
+
return headers
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_git_info(root_path):
|
|
78
|
+
# type: (str) -> dict
|
|
79
|
+
"""从项目目录获取 git 信息(URL、分支、commitID)。"""
|
|
80
|
+
info = {"gitURL": "", "gitBranch": "", "gitCommitID": ""}
|
|
81
|
+
try:
|
|
82
|
+
info["gitURL"] = subprocess.check_output(
|
|
83
|
+
["git", "remote", "get-url", "origin"], cwd=root_path, stderr=subprocess.DEVNULL
|
|
84
|
+
).decode("utf-8").strip()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
try:
|
|
88
|
+
info["gitBranch"] = subprocess.check_output(
|
|
89
|
+
["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=root_path, stderr=subprocess.DEVNULL
|
|
90
|
+
).decode("utf-8").strip()
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
try:
|
|
94
|
+
info["gitCommitID"] = subprocess.check_output(
|
|
95
|
+
["git", "rev-parse", "HEAD"], cwd=root_path, stderr=subprocess.DEVNULL
|
|
96
|
+
).decode("utf-8").strip()
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return info
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def get_output_dir(root_path, output_dir=None):
|
|
103
|
+
# type: (str, Optional[str]) -> str
|
|
104
|
+
"""获取输出目录,默认为 skill 临时目录下的项目隔离子目录。
|
|
105
|
+
|
|
106
|
+
每次调用时自动清理超过 24 小时的过期临时数据。
|
|
107
|
+
"""
|
|
108
|
+
skill_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
109
|
+
project_name = os.path.basename(os.path.realpath(root_path))
|
|
110
|
+
path_hash = hashlib.md5(os.path.realpath(root_path).encode("utf-8")).hexdigest()[:8]
|
|
111
|
+
default_output = os.path.join(skill_dir, ".tmp", "{}_{}".format(project_name, path_hash))
|
|
112
|
+
result = os.path.realpath(output_dir) if output_dir else default_output
|
|
113
|
+
|
|
114
|
+
# 清理过期临时数据
|
|
115
|
+
_cleanup_expired_tmp(skill_dir)
|
|
116
|
+
|
|
117
|
+
os.makedirs(result, exist_ok=True)
|
|
118
|
+
return result
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _cleanup_expired_tmp(skill_dir):
|
|
122
|
+
# type: (str) -> None
|
|
123
|
+
"""清理 .tmp 目录下超过 24 小时的子目录。"""
|
|
124
|
+
tmp_dir = os.path.join(skill_dir, ".tmp")
|
|
125
|
+
if not os.path.isdir(tmp_dir):
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
now = time.time()
|
|
129
|
+
try:
|
|
130
|
+
for entry in os.listdir(tmp_dir):
|
|
131
|
+
entry_path = os.path.join(tmp_dir, entry)
|
|
132
|
+
if not os.path.isdir(entry_path):
|
|
133
|
+
continue
|
|
134
|
+
try:
|
|
135
|
+
mtime = os.path.getmtime(entry_path)
|
|
136
|
+
if now - mtime > TMP_EXPIRE_SECONDS:
|
|
137
|
+
shutil.rmtree(entry_path)
|
|
138
|
+
except OSError:
|
|
139
|
+
pass
|
|
140
|
+
except OSError:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def safe_rmtree(dir_path):
|
|
145
|
+
# type: (str) -> None
|
|
146
|
+
"""安全删除临时目录,校验路径位于 .tmp 下防止误删。"""
|
|
147
|
+
dir_path = os.path.realpath(dir_path)
|
|
148
|
+
# 安全校验:确保路径在 .tmp 目录下
|
|
149
|
+
if ".tmp" not in dir_path.split(os.sep):
|
|
150
|
+
return
|
|
151
|
+
if not os.path.isdir(dir_path):
|
|
152
|
+
return
|
|
153
|
+
shutil.rmtree(dir_path)
|