keyvault-local 0.1.0__tar.gz
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.
- keyvault_local-0.1.0/LICENSE +21 -0
- keyvault_local-0.1.0/PKG-INFO +128 -0
- keyvault_local-0.1.0/README.md +117 -0
- keyvault_local-0.1.0/keyvault/__init__.py +3 -0
- keyvault_local-0.1.0/keyvault/audit.py +70 -0
- keyvault_local-0.1.0/keyvault/cli.py +383 -0
- keyvault_local-0.1.0/keyvault/config.py +37 -0
- keyvault_local-0.1.0/keyvault/gui.py +386 -0
- keyvault_local-0.1.0/keyvault/store.py +207 -0
- keyvault_local-0.1.0/keyvault/vault.py +76 -0
- keyvault_local-0.1.0/keyvault/webui.py +597 -0
- keyvault_local-0.1.0/keyvault_local.egg-info/PKG-INFO +128 -0
- keyvault_local-0.1.0/keyvault_local.egg-info/SOURCES.txt +22 -0
- keyvault_local-0.1.0/keyvault_local.egg-info/dependency_links.txt +1 -0
- keyvault_local-0.1.0/keyvault_local.egg-info/entry_points.txt +2 -0
- keyvault_local-0.1.0/keyvault_local.egg-info/requires.txt +1 -0
- keyvault_local-0.1.0/keyvault_local.egg-info/top_level.txt +1 -0
- keyvault_local-0.1.0/pyproject.toml +23 -0
- keyvault_local-0.1.0/setup.cfg +4 -0
- keyvault_local-0.1.0/tests/test_audit.py +111 -0
- keyvault_local-0.1.0/tests/test_cli.py +319 -0
- keyvault_local-0.1.0/tests/test_gui.py +147 -0
- keyvault_local-0.1.0/tests/test_store.py +148 -0
- keyvault_local-0.1.0/tests/test_vault.py +118 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 anyuer678
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: keyvault-local
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: 个人 API 密钥保险箱(KeyVault):本地加密钥匙串,专管 LLM Key / GitHub Token
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: cryptography>=50.0.0
|
|
10
|
+
Dynamic: license-file
|
|
11
|
+
|
|
12
|
+
# KeyVault —— 个人 API 密钥保险箱
|
|
13
|
+
|
|
14
|
+
> 本地加密钥匙串,专管 LLM API Key(OpenAI / DeepSeek / Anthropic)、GitHub Token、云厂商凭据。主密码(Key)不落盘。
|
|
15
|
+
|
|
16
|
+
[](https://www.python.org/)
|
|
17
|
+
[](LICENSE)
|
|
18
|
+
[](tests/)
|
|
19
|
+
[](vault.py)
|
|
20
|
+
|
|
21
|
+
比 1Password 轻、比 `.env` 安全。密钥仅以 AES-256-GCM 密文存于本地单文件 `secrets.db`。
|
|
22
|
+
|
|
23
|
+
## 功能特性
|
|
24
|
+
|
|
25
|
+
| 能力 | 说明 |
|
|
26
|
+
|---|---|
|
|
27
|
+
| 强加密 | `scrypt`(N=2¹⁵, r=8, p=1)派生主密钥 + `AES-256-GCM`,每条独立 nonce,AAD 防篡改 |
|
|
28
|
+
| 主密码不落盘 | Key 仅存进程内存,退出即失效;密码丢失 = 数据不可恢复 |
|
|
29
|
+
| 零网络 | 除打印轮换指引 URL 外不发起任何网络调用 |
|
|
30
|
+
| 只读审计 | `kv audit` 扫描 `.env*/.ini/.toml/.config` 中的明文密钥,**值永不打印** |
|
|
31
|
+
| 三种界面 | CLI / tkinter 桌面 GUI / 零依赖 Web UI(新增) |
|
|
32
|
+
| 备份恢复 | 加密备份导出 / 原子导入(校验 header,恢复需主密码) |
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install -e . # 安装 kv 命令(依赖 cryptography)
|
|
38
|
+
|
|
39
|
+
# 1. 初始化(主密码 ≥8 位;警告:密码丢失 = 数据不可恢复)
|
|
40
|
+
kv init
|
|
41
|
+
|
|
42
|
+
# 2. 加密入库(交互输入值,不回显;也可管道输入)
|
|
43
|
+
kv add deepseek --provider deepseek
|
|
44
|
+
echo -n "sk-xxx" | kv add github --provider github --stdin
|
|
45
|
+
|
|
46
|
+
# 3. 查看(永不含值)
|
|
47
|
+
kv list # 仅名称/供应商/过期日期
|
|
48
|
+
kv get deepseek # 默认摘要 sk-…****h4
|
|
49
|
+
kv get deepseek --full # 仅显式请求时输出完整值
|
|
50
|
+
|
|
51
|
+
# 4. 注入当前 shell(输出 export 行,请用 eval 消费)
|
|
52
|
+
eval "$(kv use deepseek)"
|
|
53
|
+
|
|
54
|
+
# 5. 审计明文密钥(只读,值打码)
|
|
55
|
+
kv audit --dir ~/myproject
|
|
56
|
+
|
|
57
|
+
# 6. 备份 / 恢复(加密备份,恢复需主密码)
|
|
58
|
+
kv export-backup backup.db
|
|
59
|
+
kv import-backup backup.db # 原子替换,需二次确认
|
|
60
|
+
|
|
61
|
+
# 7. 桌面 GUI / Web UI
|
|
62
|
+
kv gui
|
|
63
|
+
python webui.py # 浏览器打开 http://127.0.0.1:8765
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## 命令速查
|
|
67
|
+
|
|
68
|
+
| 命令 | 说明 |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `kv init` / `kv unlock` | 创建 / 解锁 vault(密码经 stdin 或环境变量 `KV_PASS`) |
|
|
71
|
+
| `kv add <name> [--provider p] [--expires YYYY-MM-DD] [--stdin]` | 加密入库 |
|
|
72
|
+
| `kv get <name> [--full]` | 解密输出(默认只显示前 4 后 4) |
|
|
73
|
+
| `kv list` | 列出名称/供应商/过期日期(永不含值) |
|
|
74
|
+
| `kv use <name> [--shell bash\|zsh\|fish] [--dotenv FILE]` | 输出 export 行或写入 0600 的 .env |
|
|
75
|
+
| `kv edit / kv delete [--yes]` | 修改 / 删除(二次确认) |
|
|
76
|
+
| `kv audit [--dir .] [--json] [--fix-0600]` | 扫描明文密钥(只读) |
|
|
77
|
+
| `kv rotate --provider <p>` | 打印官方轮换指引 URL(不联网) |
|
|
78
|
+
| `kv export-backup <file>` / `kv import-backup <file>` | 加密备份导出 / 原子导入 |
|
|
79
|
+
| `kv gui` | 启动 tkinter 桌面前端 |
|
|
80
|
+
|
|
81
|
+
## 环境变量
|
|
82
|
+
|
|
83
|
+
| 变量 | 用途 |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `KV_PASS` | 主密码(避免交互输入;仅限可信环境,勿写进 shell 历史) |
|
|
86
|
+
| `KV_DIR` | vault 目录(默认 `~/.keyvault`) |
|
|
87
|
+
| `KV_DB` | vault 数据库文件路径(默认 `<KV_DIR>/secrets.db`) |
|
|
88
|
+
|
|
89
|
+
## 安全模型与边界
|
|
90
|
+
|
|
91
|
+
- 主密钥由 `scrypt(主密码, 随机盐)` 派生,**从不落盘**,仅存进程内存,退出即失效。
|
|
92
|
+
- 密文用 AES-256-GCM,AAD = name+provider 防篡改;篡改 ciphertext/tag/nonce/名称 → 解密抛 `ValueError`。
|
|
93
|
+
- 库文件与备份文件创建时设为 0600(Windows 上 `os.chmod` 仅影响只读位,请配合磁盘加密/ACL)。
|
|
94
|
+
- `kv list` 永不返回密钥值;`kv audit` 仅报告文件名+key 名,值区打码 `REDACTED`。
|
|
95
|
+
- **主密码丢失 = 数据不可恢复**(无后门、无主密钥恢复机制),请备份加密库。
|
|
96
|
+
|
|
97
|
+
## 项目结构
|
|
98
|
+
|
|
99
|
+
```
|
|
100
|
+
cli.py 全部子命令(kv 入口)
|
|
101
|
+
vault.py 加解密核心(scrypt + AES-GCM,纯函数无 IO)
|
|
102
|
+
store.py SQLite 持久化(0600 权限、原子写、导入校验)
|
|
103
|
+
audit.py 明文密钥扫描(只读,值永不返回)
|
|
104
|
+
gui.py tkinter 桌面 GUI(延迟 import)
|
|
105
|
+
webui.py 零依赖 Web 前端(内嵌 HTML/JS/CSS)
|
|
106
|
+
config.py vault 路径/目录权限
|
|
107
|
+
tests/ pytest 测试(81 例,全 mock 不触网)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## 测试
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python -m pytest tests/ -v
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## 隐私与免责
|
|
117
|
+
|
|
118
|
+
- 本工具除打印轮换指引 URL 外零网络调用;主密码与密钥值均不出本机。
|
|
119
|
+
- 完整值输出走 stdout,请勿重定向到文件后长期保存;`kv use --dotenv` 生成的文件请用后删除,勿提交 git。
|
|
120
|
+
- 加密算法与实现经过测试验证,但不构成安全审计结论;存放高价值密钥请额外评估威胁模型。
|
|
121
|
+
|
|
122
|
+
本项目仅供学习交流与演示用途,不构成任何形式的商业服务或技术承诺。软件按「现状」提供,不作任何明示或暗示的保证,包括但不限于适销性、特定用途适用性与非侵权性。
|
|
123
|
+
您理解并同意:使用本项目即表示您自行承担全部风险。如您在使用过程中发现缺陷或问题,欢迎通过 GitHub Issues 反馈,但作者不因使用本软件所直接或间接产生的任何损失(包括但不限于数据丢失、业务中断、第三方索赔)承担责任。
|
|
124
|
+
本项目以功能演示与学习交流为主要目的,其架构设计、安全基线、容错机制与性能表现均未按生产级标准进行验证与加固,不适用于实际生产环境或关键业务场景。任何将本项目部署于生产系统、对外提供服务、或将其接入真实业务工作流的做法,均属使用者的自主决策行为;由此产生的任何直接或间接不良后果,包括但不限于服务中断、数据损坏或泄露、业务损失、合规风险、以及因依赖本软件而引发的第三方纠纷,**开发者均不承担任何责任**。若您确有生产级使用需求,请在充分评估与自行加固(包括但不限于安全审计、压力测试、代码审查)后,自行承担相应风险。
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
[GPL-3.0](LICENSE) — Copyright (C) 2026 anyuer678
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# KeyVault —— 个人 API 密钥保险箱
|
|
2
|
+
|
|
3
|
+
> 本地加密钥匙串,专管 LLM API Key(OpenAI / DeepSeek / Anthropic)、GitHub Token、云厂商凭据。主密码(Key)不落盘。
|
|
4
|
+
|
|
5
|
+
[](https://www.python.org/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](tests/)
|
|
8
|
+
[](vault.py)
|
|
9
|
+
|
|
10
|
+
比 1Password 轻、比 `.env` 安全。密钥仅以 AES-256-GCM 密文存于本地单文件 `secrets.db`。
|
|
11
|
+
|
|
12
|
+
## 功能特性
|
|
13
|
+
|
|
14
|
+
| 能力 | 说明 |
|
|
15
|
+
|---|---|
|
|
16
|
+
| 强加密 | `scrypt`(N=2¹⁵, r=8, p=1)派生主密钥 + `AES-256-GCM`,每条独立 nonce,AAD 防篡改 |
|
|
17
|
+
| 主密码不落盘 | Key 仅存进程内存,退出即失效;密码丢失 = 数据不可恢复 |
|
|
18
|
+
| 零网络 | 除打印轮换指引 URL 外不发起任何网络调用 |
|
|
19
|
+
| 只读审计 | `kv audit` 扫描 `.env*/.ini/.toml/.config` 中的明文密钥,**值永不打印** |
|
|
20
|
+
| 三种界面 | CLI / tkinter 桌面 GUI / 零依赖 Web UI(新增) |
|
|
21
|
+
| 备份恢复 | 加密备份导出 / 原子导入(校验 header,恢复需主密码) |
|
|
22
|
+
|
|
23
|
+
## 快速开始
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e . # 安装 kv 命令(依赖 cryptography)
|
|
27
|
+
|
|
28
|
+
# 1. 初始化(主密码 ≥8 位;警告:密码丢失 = 数据不可恢复)
|
|
29
|
+
kv init
|
|
30
|
+
|
|
31
|
+
# 2. 加密入库(交互输入值,不回显;也可管道输入)
|
|
32
|
+
kv add deepseek --provider deepseek
|
|
33
|
+
echo -n "sk-xxx" | kv add github --provider github --stdin
|
|
34
|
+
|
|
35
|
+
# 3. 查看(永不含值)
|
|
36
|
+
kv list # 仅名称/供应商/过期日期
|
|
37
|
+
kv get deepseek # 默认摘要 sk-…****h4
|
|
38
|
+
kv get deepseek --full # 仅显式请求时输出完整值
|
|
39
|
+
|
|
40
|
+
# 4. 注入当前 shell(输出 export 行,请用 eval 消费)
|
|
41
|
+
eval "$(kv use deepseek)"
|
|
42
|
+
|
|
43
|
+
# 5. 审计明文密钥(只读,值打码)
|
|
44
|
+
kv audit --dir ~/myproject
|
|
45
|
+
|
|
46
|
+
# 6. 备份 / 恢复(加密备份,恢复需主密码)
|
|
47
|
+
kv export-backup backup.db
|
|
48
|
+
kv import-backup backup.db # 原子替换,需二次确认
|
|
49
|
+
|
|
50
|
+
# 7. 桌面 GUI / Web UI
|
|
51
|
+
kv gui
|
|
52
|
+
python webui.py # 浏览器打开 http://127.0.0.1:8765
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 命令速查
|
|
56
|
+
|
|
57
|
+
| 命令 | 说明 |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `kv init` / `kv unlock` | 创建 / 解锁 vault(密码经 stdin 或环境变量 `KV_PASS`) |
|
|
60
|
+
| `kv add <name> [--provider p] [--expires YYYY-MM-DD] [--stdin]` | 加密入库 |
|
|
61
|
+
| `kv get <name> [--full]` | 解密输出(默认只显示前 4 后 4) |
|
|
62
|
+
| `kv list` | 列出名称/供应商/过期日期(永不含值) |
|
|
63
|
+
| `kv use <name> [--shell bash\|zsh\|fish] [--dotenv FILE]` | 输出 export 行或写入 0600 的 .env |
|
|
64
|
+
| `kv edit / kv delete [--yes]` | 修改 / 删除(二次确认) |
|
|
65
|
+
| `kv audit [--dir .] [--json] [--fix-0600]` | 扫描明文密钥(只读) |
|
|
66
|
+
| `kv rotate --provider <p>` | 打印官方轮换指引 URL(不联网) |
|
|
67
|
+
| `kv export-backup <file>` / `kv import-backup <file>` | 加密备份导出 / 原子导入 |
|
|
68
|
+
| `kv gui` | 启动 tkinter 桌面前端 |
|
|
69
|
+
|
|
70
|
+
## 环境变量
|
|
71
|
+
|
|
72
|
+
| 变量 | 用途 |
|
|
73
|
+
|---|---|
|
|
74
|
+
| `KV_PASS` | 主密码(避免交互输入;仅限可信环境,勿写进 shell 历史) |
|
|
75
|
+
| `KV_DIR` | vault 目录(默认 `~/.keyvault`) |
|
|
76
|
+
| `KV_DB` | vault 数据库文件路径(默认 `<KV_DIR>/secrets.db`) |
|
|
77
|
+
|
|
78
|
+
## 安全模型与边界
|
|
79
|
+
|
|
80
|
+
- 主密钥由 `scrypt(主密码, 随机盐)` 派生,**从不落盘**,仅存进程内存,退出即失效。
|
|
81
|
+
- 密文用 AES-256-GCM,AAD = name+provider 防篡改;篡改 ciphertext/tag/nonce/名称 → 解密抛 `ValueError`。
|
|
82
|
+
- 库文件与备份文件创建时设为 0600(Windows 上 `os.chmod` 仅影响只读位,请配合磁盘加密/ACL)。
|
|
83
|
+
- `kv list` 永不返回密钥值;`kv audit` 仅报告文件名+key 名,值区打码 `REDACTED`。
|
|
84
|
+
- **主密码丢失 = 数据不可恢复**(无后门、无主密钥恢复机制),请备份加密库。
|
|
85
|
+
|
|
86
|
+
## 项目结构
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
cli.py 全部子命令(kv 入口)
|
|
90
|
+
vault.py 加解密核心(scrypt + AES-GCM,纯函数无 IO)
|
|
91
|
+
store.py SQLite 持久化(0600 权限、原子写、导入校验)
|
|
92
|
+
audit.py 明文密钥扫描(只读,值永不返回)
|
|
93
|
+
gui.py tkinter 桌面 GUI(延迟 import)
|
|
94
|
+
webui.py 零依赖 Web 前端(内嵌 HTML/JS/CSS)
|
|
95
|
+
config.py vault 路径/目录权限
|
|
96
|
+
tests/ pytest 测试(81 例,全 mock 不触网)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 测试
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python -m pytest tests/ -v
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## 隐私与免责
|
|
106
|
+
|
|
107
|
+
- 本工具除打印轮换指引 URL 外零网络调用;主密码与密钥值均不出本机。
|
|
108
|
+
- 完整值输出走 stdout,请勿重定向到文件后长期保存;`kv use --dotenv` 生成的文件请用后删除,勿提交 git。
|
|
109
|
+
- 加密算法与实现经过测试验证,但不构成安全审计结论;存放高价值密钥请额外评估威胁模型。
|
|
110
|
+
|
|
111
|
+
本项目仅供学习交流与演示用途,不构成任何形式的商业服务或技术承诺。软件按「现状」提供,不作任何明示或暗示的保证,包括但不限于适销性、特定用途适用性与非侵权性。
|
|
112
|
+
您理解并同意:使用本项目即表示您自行承担全部风险。如您在使用过程中发现缺陷或问题,欢迎通过 GitHub Issues 反馈,但作者不因使用本软件所直接或间接产生的任何损失(包括但不限于数据丢失、业务中断、第三方索赔)承担责任。
|
|
113
|
+
本项目以功能演示与学习交流为主要目的,其架构设计、安全基线、容错机制与性能表现均未按生产级标准进行验证与加固,不适用于实际生产环境或关键业务场景。任何将本项目部署于生产系统、对外提供服务、或将其接入真实业务工作流的做法,均属使用者的自主决策行为;由此产生的任何直接或间接不良后果,包括但不限于服务中断、数据损坏或泄露、业务损失、合规风险、以及因依赖本软件而引发的第三方纠纷,**开发者均不承担任何责任**。若您确有生产级使用需求,请在充分评估与自行加固(包括但不限于安全审计、压力测试、代码审查)后,自行承担相应风险。
|
|
114
|
+
|
|
115
|
+
## License
|
|
116
|
+
|
|
117
|
+
[GPL-3.0](LICENSE) — Copyright (C) 2026 anyuer678
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""audit.py — .env 明文密钥扫描(只读,不写任何文件)。"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
KEY_NAME_PATTERN = re.compile(
|
|
8
|
+
r"(OPENAI|ANTHROPIC|DEEPSEEK|GITHUB|AWS|AZURE|GOOGLE|ZHIPU|MOONSHOT|QWEN|DOUBAO|API)_?(KEY|TOKEN|SECRET)",
|
|
9
|
+
re.IGNORECASE,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
_TARGET_SUFFIXES = (".env", ".ini", ".toml", ".config", ".yaml", ".yml", ".json", ".ps1")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Finding:
|
|
17
|
+
file: str
|
|
18
|
+
key_name: str
|
|
19
|
+
redacted: bool = True
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def scan_envline(line: str) -> Finding | None:
|
|
23
|
+
"""单行判定:KEY=value / KEY: value(yaml/json)/ $env:NAME = value(ps1),key 名匹配模式。
|
|
24
|
+
|
|
25
|
+
= 与 : 两种分隔符都尝试,任一拆分出的 key 命中即报告。
|
|
26
|
+
"""
|
|
27
|
+
line = line.strip()
|
|
28
|
+
if not line or line.startswith("#"):
|
|
29
|
+
return None
|
|
30
|
+
for sep in ("=", ":"):
|
|
31
|
+
i = line.find(sep)
|
|
32
|
+
if i == -1:
|
|
33
|
+
continue
|
|
34
|
+
key = line[:i].strip().strip('"\'{}$')
|
|
35
|
+
key = re.sub(r"^env:", "", key, flags=re.IGNORECASE).strip()
|
|
36
|
+
if not key:
|
|
37
|
+
continue
|
|
38
|
+
if KEY_NAME_PATTERN.search(key):
|
|
39
|
+
return Finding(file="", key_name=key, redacted=True)
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def scan_dir(root: str, *, max_files: int = 10_000,
|
|
44
|
+
ignore: tuple[str, ...] = (".git", "node_modules")) -> list[Finding]:
|
|
45
|
+
"""递归扫描目标后缀文件,命中 KEY=value 即报告,值区永不返回。"""
|
|
46
|
+
findings: list[Finding] = []
|
|
47
|
+
count = 0
|
|
48
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
49
|
+
dirnames[:] = [d for d in dirnames if d not in ignore]
|
|
50
|
+
for fn in filenames:
|
|
51
|
+
if count >= max_files:
|
|
52
|
+
return findings
|
|
53
|
+
count += 1
|
|
54
|
+
if not _is_target(fn):
|
|
55
|
+
continue
|
|
56
|
+
path = os.path.join(dirpath, fn)
|
|
57
|
+
try:
|
|
58
|
+
with open(path, "r", encoding="utf-8", errors="replace") as fh:
|
|
59
|
+
for line in fh:
|
|
60
|
+
hit = scan_envline(line)
|
|
61
|
+
if hit is not None:
|
|
62
|
+
hit.file = path
|
|
63
|
+
findings.append(hit)
|
|
64
|
+
except OSError:
|
|
65
|
+
continue
|
|
66
|
+
return findings
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_target(fn: str) -> bool:
|
|
70
|
+
return fn.startswith(".env") or fn.lower().endswith(_TARGET_SUFFIXES)
|