105 lines
3.6 KiB
Python
105 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Vault 全量体检 — 一级目录统计 / 编码变体 / 明文凭证 / 空目录 / 项目误入。
|
|
|
|
用法: python3 vault-audit.py [VAULT_DIR] # 默认 ~/mc
|
|
只读扫描,不做任何修改。输出中的凭证值一律脱敏。
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
VAULT = os.path.expanduser(sys.argv[1] if len(sys.argv) > 1 else '~/mc')
|
|
EXCL = {'.git', '.obsidian', '.smart-env', '__pycache__', 'node_modules', '.trash'}
|
|
PROJ_MARK = ('.git', 'Dockerfile', 'package.json', 'go.mod', 'pyproject.toml', 'Cargo.toml', 'manage.py')
|
|
CRED_PATS = [
|
|
r'sk-[A-Za-z0-9\-_.]{24,}',
|
|
r'nvapi-[A-Za-z0-9\-_.]{24,}',
|
|
r'tskey-[a-z]+-[A-Za-z0-9\-_]{15,}',
|
|
r'gh[pousr]_[A-Za-z0-9]{30,}',
|
|
r'cli_[a-z0-9]{16,}',
|
|
r'AIza[A-Za-z0-9\-_]{30,}',
|
|
]
|
|
ENC_MARK = ('%E', '\\u', 'ç', 'Ã', '____') # URL编码 / Unicode字面量 / 双重编码 / 乱码文件名
|
|
TEXT_EXT = ('.md', '.txt', '.json', '.yaml', '.yml', '.env', '.py', '.sh', '.conf', '.ini')
|
|
|
|
|
|
def masked(v):
|
|
return (v[:8] + '…' + v[-4:]) if len(v) > 14 else (v[:4] + '…')
|
|
|
|
|
|
def walkstat(path):
|
|
nf = sz = 0
|
|
for root, dirs, files in os.walk(path):
|
|
dirs[:] = [d for d in dirs if d not in EXCL]
|
|
for f in files:
|
|
try:
|
|
sz += os.path.getsize(os.path.join(root, f))
|
|
nf += 1
|
|
except OSError:
|
|
pass
|
|
return nf, sz
|
|
|
|
|
|
def main():
|
|
print(f"=== Vault 体检: {VAULT} ===\n\n## 一级目录")
|
|
for t in sorted(os.listdir(VAULT)):
|
|
if t.startswith('.'):
|
|
continue
|
|
p = os.path.join(VAULT, t)
|
|
if os.path.isfile(p):
|
|
print(f" [文件] {t} ({os.path.getsize(p)}B) <- 根目录散落文件")
|
|
continue
|
|
nf, sz = walkstat(p)
|
|
proj = [m for m in PROJ_MARK if os.path.exists(os.path.join(p, m))]
|
|
tag = f" [!] 项目特征 {','.join(proj)} -> 应移 ~/projects/" if proj else ""
|
|
print(f" [目录] {t}/ {nf} 文件 {sz / 1048576:.1f}MB{tag}")
|
|
|
|
print("\n## 编码变体目录(同一逻辑名多种写法 = 编码灾难)")
|
|
hit = False
|
|
for root, dirs, files in os.walk(VAULT):
|
|
if any(e in root for e in EXCL):
|
|
continue
|
|
for d in dirs:
|
|
if any(m in d for m in ENC_MARK):
|
|
fp = os.path.join(root, d)
|
|
try:
|
|
n = len(os.listdir(fp))
|
|
except OSError:
|
|
n = 0
|
|
print(f" [!] {fp.replace(VAULT + '/', '')} ({n} 项)")
|
|
hit = True
|
|
if not hit:
|
|
print(" OK 无")
|
|
|
|
print("\n## 明文凭证分布(值已脱敏)")
|
|
for root, dirs, files in os.walk(VAULT):
|
|
if any(e in root for e in EXCL):
|
|
continue
|
|
for f in files:
|
|
if not f.endswith(TEXT_EXT):
|
|
continue
|
|
fp = os.path.join(root, f)
|
|
try:
|
|
c = open(fp, encoding='utf-8', errors='ignore').read()
|
|
except OSError:
|
|
continue
|
|
found = set()
|
|
for pat in CRED_PATS:
|
|
for m in re.findall(pat, c):
|
|
if m.endswith(('.md', 'qwen')) or 'placeholder' in m.lower():
|
|
continue
|
|
found.add(masked(m))
|
|
if found:
|
|
print(f" {fp.replace(VAULT + '/', '')} -> {', '.join(sorted(found)[:6])}")
|
|
|
|
print("\n## 空目录")
|
|
for root, dirs, files in os.walk(VAULT):
|
|
if any(e in root for e in EXCL):
|
|
continue
|
|
if not dirs and not files and root != VAULT:
|
|
print(f" [空] {root.replace(VAULT + '/', '')}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|