Skill: 13256659129/sx-security-audit-1-0-0 (version 1.0.0)

--- name: SX-security-audit description: 全方位安全审计技能。检查文件权限、环境变量、依赖漏洞、配置文件、网络端口、Git 安全、Shell 安全、macOS 安全、密钥检测等。支持 CLI 参数、JSON 输出、配置文件。当用户要求"安全检查"、"漏洞扫描"、"权限检查"、"安全审计"时使用此技能。 --- # Security Audit - 全方位安全审计技能 本技能提供全面的安全检查功能，涵盖系统、代码、配置等多个层面，并支持生成报告发送到飞书。 ## 检查范围 本技能支持以下安全检查模块（可通过 `--check` 选择）： ### 🔒 permissions - 权限与访问控制 - **敏感文件权限** - 检查 .ssh、.aws、.gnupg、OpenClaw 目录权限 - 使用位掩码 `actual_mode & ~expected_mode` 精确检测超出预期的权限位 ### 🔓 world_writable - 世界可写文件检测 - 在 workspace 目录下扫描 o+w 权限的文件 - 检测 SUID/SGID 位设置的文件 ### 🔐 config - OpenClaw 配置安全 - 使用正则模式检测配置文件中的真实密钥（AWS、GitHub、Slack、OpenAI 等） - 检查 Gateway 认证方式 - 检查插件安装状态 ### 📝 skills - Skills 代码安全 - 用正则模式匹配检测硬编码密钥 - 检查不安全的函数调用（eval、exec、os.system 等） - 高熵字符串检测（Shannon entropy > 4.5） ### 📦 dependencies - 依赖与供应链安全 - 运行 `npm audit --json` 检查 NPM 依赖漏洞 - 正确处理 npm audit 非零返回码（有漏洞时返回非零） - 按漏洞严重级别统计 ### 🌍 env - 环境变量安全 - 扫描当前进程环境变量中的敏感信息 - 检测已知 API key 格式（sk-、ghp_、xoxb-、AKIA 等） - 检测环境变量名称中的敏感关键词 ### 🔀 git - Git 安全检查 - 检查 .gitignore 是否忽略了 .env、.secret、credentials 等 - 检查 git credential.helper 是否使用明文存储 - 扫描最近 commit diff 中的密钥模式 ### 🌐 network - 网络端口扫描 - 使用 `lsof -i -P -n` 检查监听端口 - 标记监听 0.0.0.0（全接口暴露）的进程 - 列出所有监听端口清单 ### 🐚 shell - Shell 安全检查 - 检查 .bash_history / .zsh_history 文件权限 - 扫描 .bashrc / .zshrc 中的明文 export 密钥 ### 🍎 macos - macOS 安全检查（仅 macOS） - 检查防火墙状态（socketfilterfw） - 检查 Gatekeeper 状态（spctl） - 检查 SIP 系统完整性保护状态（csrutil） ## 增强密钥检测 使用正则模式精确匹配已知密钥格式： | 类型 | 模式 | |------|------| | AWS Access Key | `AKIA[0-9A-Z]{16}` | | GitHub Token | `ghp_[a-zA-Z0-9]{36}` / `github_pat_` | | Slack Token | `xox[bpas]-` | | OpenAI/Anthropic Key | `sk-[a-zA-Z0-9]{20,}` | | JWT Token | `eyJ[a-zA-Z0-9_-]+\.eyJ` | | Private Key | `-----BEGIN PRIVATE KEY-----` | 同时支持 Shannon 信息熵检测（> 4.5 的高熵字符串视为可疑密钥）。 ## CLI 使用方式 ### 完整审计 ```bash python3 scripts/security_audit.py ``` ### 指定检查模块 ```bash python3 scripts/security_audit.py --check permissions env git ``` ### 过滤严重级别 ```bash python3 scripts/security_audit.py --severity high ``` ### JSON 输出 ```bash python3 scripts/security_audit.py --json python3 scripts/security_audit.py --json --output report.json ``` ### 列出可用模块 ```bash python3 scripts/security_audit.py --list-checks ``` ### 静默模式 ```bash python3 scripts/security_audit.py --quiet --json --output report.json ``` ## 配置文件支持 创建 `.security-audit.json` 自定义审计行为： ```json { "excludePaths": [ "/tmp", "node_modules" ], "severityThreshold": "medium", "autoFix": false, "reportFormat": "markdown" } ``` 配置文件搜索顺序： 1. 当前目录 `.security-audit.json` 2. `~/.openclaw/.security-audit.json` 3. `~/.openclaw/workspace/.security-audit.json` ## 飞书报告 ### 发送报告 ```bash # 富文本格式（默认） python3 scripts/send_report_to_feishu.py report.md # 卡片格式 python3 scripts/send_report_to_feishu.py report.md --format card # 指定 Webhook python3 scripts/send_report_to_feishu.py report.md --webhook https://... ``` ### 支持的消息格式 - **text** - 纯文本消息 - **rich** - 飞书富文本（post 类型），包含标题、加粗等样式 - **card** - 交互式卡片，支持颜色标签（红/橙/绿），摘要卡片化展示 ### 发送方式优先级 1. OpenClaw 插件 API（如已配置飞书插件） 2. 飞书 Webhook（通过 `--webhook` 或 `FEISHU_WEBHOOK_URL` 环境变量） 3. 保存格式化消息供手动发送 ## 报告格式 审计报告包含以下部分： ### 📊 执行摘要 - 审计时间、检查范围 - 通过/警告/失败统计 - 风险等级分布（严重/高/中/低） ### 🔍 详细检查结果 每个检查项包含：状态、风险等级、问题描述、影响范围、修复建议 ### 📋 优先修复清单 按风险等级排序的问题列表，附带修复命令 ## 参考资料 - [权限检查规则](references/permissions.md) - [密钥检测规则](references/secrets-detection.md) - [依赖漏洞扫描](references/dependency-audit.md) - [代码安全最佳实践](references/code-security.md) ## 注意事项 - 首次运行可能需要安装额外依赖（`npm audit` 等） - 某些检查需要 sudo/root 权限（如完整端口扫描） - macOS 安全检查仅在 Darwin 平台执行 - 报告发送到飞书需要飞书插件已配置或提供 Webhook URL --- **技能维护者**: 小小宝 🐾✨ **最后更新**: 2026-03-12

path: _meta.json
content: ownerId: kn7430vg2m3x4kthgg87bfw7bh82kmbh
slug: sx-security-audit
version: 1.0.0
publishedAt: 1773283302741

path: references/code-security.md
content: # 代码安全最佳实践

## 通用安全编码原则

### 1. 输入验证
✅ **永远验证用户输入**
```javascript
// ❌ 不好 - 直接使用
const query = req.params.q;
db.execute(`SELECT * FROM users WHERE name = '${query}'`);

// ✅ 好 - 参数化查询
const query = req.params.q;
db.execute('SELECT * FROM users WHERE name = ?', [query]);
```

### 2. 输出编码
✅ **防止 XSS，编码输出**
```javascript
// ❌ 不好
document.innerHTML = userComment;

// ✅ 好
document.textContent = userComment;
// 或
document.innerHTML = escapeHtml(userComment);
```

### 3. 最小权限原则
✅ **只授予必要的权限**
```python
# ❌ 不好 - 全可读
chmod 755 /path/to/file

# ✅ 好 - 最小权限
chmod 640 /path/to/file  # 所有者读写，组只读
```

### 4. 深度防御
✅ **多层安全控制**
```javascript
// 即使一层验证失败，仍有其他保护
if (!validateInput(userInput)) {
  throw new Error('Invalid input');
}
if (!hasPermission(user, resource)) {
  throw new Error('Unauthorized');
}
if (!isValidSession(session)) {
  throw new Error('Invalid session');
}
```

## 静态分析规则

### 危险函数检测

以下函数需要额外审查：

#### JavaScript/Node.js
| 函数 | 风险 | 替代方案 |
|-------|------|----------|
| `eval()` | 代码注入 | 避免使用，用 JSON.parse 或构建 AST |
| `Function()` | 代码注入 | 避免使用 |
| `setTimeout(code)` | 代码注入 | `setTimeout(() => {}, delay)` |
| `child_process.exec()` | 命令注入 | `execFile()` + 参数数组 |
| `fs.writeFileSync()` | 路径遍历 | 验证路径，使用 `path.resolve()` |
| `require(user_input)` | 模块注入 | 白名单验证模块名 |

#### Python
| 函数 | 风险 | 替代方案 |
|-------|------|----------|
| `eval()` | 代码注入 | 避免使用，用 `exec()` 或 `ast.literal_eval()` |
| `exec()` | 命令注入 | 使用 `subprocess.run()` + 参数数组 |
| `os.system()` | 命令注入 | 使用 `subprocess` 模块 |
| `__import__(user_input)` | 模块导入 | 白名单验证 |
| `pickle.loads()` | 不安全反序列化 | 使用 `json` 或 `yaml` |
| `input()` (Python 2) | 未验证输入 | Python 3 中已移除 |

#### Bash/Shell
| 命令 | 风险 | 替代方案 |
|-------|------|----------|
| `eval` | 代码注入 | 完全避免 |
| `$var` (未引号) | 命令注入 | `"$var"` 或 `${var@Q}` |
| `source $file` | 命令注入 | 验证文件路径 |

### 不安全模式检测

#### 1. 硬编码密钥
```javascript
// ❌ 检测到
const [REDACTED_SECRET]";

// ✅ 推荐
const [REDACTED_SECRET];
```

#### 2. SQL 注入模式
```javascript
// ❌ 检测到
db.query(`SELECT * FROM users WHERE id = ${userId}`);

// ✅ 推荐
db.query('SELECT * FROM users WHERE id = ?', [userId]);
```

#### 3. 路径遍历模式
```javascript
// ❌ 检测到
const filePath = `./uploads/${req.params.filename}`;
fs.readFile(filePath);

// ✅ 推荐
import path from 'path';
const filePath = path.join('./uploads', req.params.filename);
const safePath = path.normalize(filePath);
if (!safePath.startsWith('./uploads/')) {
  throw new Error('Invalid path');
}
```

#### 4. 不安全的重定向
```javascript
// ❌ 检测到
res.redirect(userInput);

// ✅ 推荐
const allowedDomains = ['example.com', 'trusted.com'];
const url = new URL(userInput);
if (allowedDomains.includes(url.hostname)) {
  res.redirect(userInput);
}
```

## OpenClaw/Skills 特定规则

### 1. 子进程执行检查
```python
# ❌ 不推荐 - 直接执行用户输入
subprocess.run(user_command, shell=True)

# ✅ 推荐 - 使用参数列表
subprocess.run(['ls', '-la', path], shell=False)
```

### 2. 文件操作检查
```python
# ❌ 不推荐 - 未验证路径
with open(user_input, 'r') as f:
    ...

# ✅ 推荐 - 验证和规范化路径
safe_path = Path(user_input).resolve()
if str(safe_path).startswith('/allowed/'):
    with open(safe_path, 'r') as f:
        ...
```

### 3. 网络请求检查
```python
# ❌ 不推荐 - 未验证 URL
urllib.request.open(user_input)

# ✅ 推荐 - 验证 URL
from urllib.parse import urlparse
parsed = urlparse(user_input)
if parsed.netloc in ALLOWED_HOSTS:
    urllib.request.open(user_input)
```

### 4. 技能间数据传递检查
```json
// ❌ 不推荐 - 通过命令行传递敏感信息
{
  "command": "my-tool",
  "args": ["--api-key", "sk-abc123"]
}

// ✅ 推荐 - 使用环境变量
{
  "command": "my-tool",
  "env": {
    "API_KEY": "sk-abc123"
  }
}
```

## 代码审查清单

### 新技能开发时检查

- [ ] 是否验证所有外部输入？
- [ ] 是否使用参数化查询（防 SQLi）？
- [ ] 是否编码所有输出（防 XSS）？
- [ ] 是否避免 `eval()`、`exec()` 等危险函数？
- [ ] 是否使用最小权限原则？
- [ ] 是否记录错误但不泄露敏感信息？
- [ ] 是否使用 HTTPS 而非 HTTP？
- [ ] 是否验证重定向 URL？
- [ ] 是否使用安全的随机数生成？
- [ ] 是否正确处理异常？

### 静态分析工具集成

#### ESLint + Security Plugin (JavaScript)
```bash
# 安装
npm install eslint-plugin-security

# 配置 .eslintrc.json
{
  "plugins": ["security"],
  "rules": {
    "security/detect-eval-with-expression": "error",
    "security/detect-no-csrf-before-method-override": "error",
    "security/detect-object-injection": "error",
    "security/detect-unsafe-regex": "warn"
  }
}
```

#### Semgrep (多语言）
```bash
# 安装
pip install semgrep

# 扫描
semgrep --config=auto ./skills/
```

#### Snyk (依赖 + 代码）
```bash
# 安装
npm install -g snyk

# 扫描
snyk test
```

## 代码质量与安全

### 避免的安全反模式

#### 1. 魔法数字
```python
# ❌ 不好 - 硬编码密钥
if api_key == "12345":
    return True

# ✅ 好 - 使用安全比较
import hmac
return hmac.compare_digest(api_key, expected_key)
```

#### 2. 时间比较
```python
# ❌ 不好 - 可能被绕过
if stored_hash == computed_hash:
    return True

# ✅ 好 - 恒定时间比较
return hmac.compare_digest(stored_hash, computed_hash)
```

#### 3. 随机数生成
```javascript
// ❌ 不好 - 可预测
const randomId = Math.random();

// ✅ 好 - 加密安全
const randomId = crypto.randomBytes(16).toString('hex');
```

### 日志安全

#### ✅ 安全日志
```javascript
// 记录事件，不记录敏感数据
logger.info('User login', {
  userId: user.id,
  username: user.username,
  // ❌ 不要记录
  // [REDACTED_SECRET],
  // [REDACTED_SECRET]
});
```

#### ❌ 不安全日志
```javascript
// 避免记录敏感信息
logger.info(`User login: ${username}:${password}:${token}`);
```

## 安全测试

### 单元测试安全场景

```python
# 测试输入验证
def test_sql_injection_protection():
    malicious_input = "'; DROP TABLE users; --"
    response = api.get_user(malicious_input)
    assert response.status_code != 500  # 不应崩溃
    
# 测试权限
def test_unauthorized_access():
    response = client.get('/admin/data')
    assert response.status_code == 401
```

### 模糊测试
```bash
# 使用 API Fuzzer
npm install -g api-fuzzer
api-fuzzer https://api.example.com

# 手动模糊测试
for i in range(1000):
    send_request(fuzzed_data(i))
```

## 持续改进

### 安全培训资源

- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [CWE Top 25](https://cwe.mitre.org/top25/)
- [SANS Top 25](https://www.sans.org/top25-software-errors/)
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security)
- [Python Security Guide](https://python.readthedocs.io/en/latest/security/index.html)

### 定期审计计划

1. **每周**: 运行静态分析工具
2. **每月**: 人工代码审查
3. **每季度**: 依赖漏洞扫描
4. **每年**: 渗透测试（生产）

---
**维护者**: 小小宝 🐾✨
**最后更新**: 2026-03-11


path: references/dependency-audit.md
content: # 依赖漏洞扫描

## NPM 依赖安全

### 检查命令

#### 基础审计
```bash
npm audit
```

#### JSON 输出（推荐）
```bash
npm audit --json
```

#### 自动修复（谨慎使用）
```bash
npm audit fix
```

#### 强制修复（可能破坏兼容性）
```bash
npm audit fix --force
```

### 审计结果解读

| 严重程度 | 描述 | 处理建议 |
|-----------|-------|-----------|
| Critical | 严重安全漏洞，可能导致 RCE | 立即修复 |
| High | 高危漏洞，可能导致数据泄露 | 24小时内修复 |
| Moderate | 中危漏洞，需关注 | 下个版本修复 |
| Low | 低危漏洞，信息泄露风险 | 计划修复 |
| Info | 信息性提示 | 评估后决定 |

### 常见 NPM 漏洞类型

#### 1. 原型污染污染 (Prototype Pollution)
- **风险**: 可绕过安全检查，执行任意代码
- **影响**: 特别是在对象合并操作中
- **修复**: 使用安全的合并库，更新到最新版本

#### 2. 正则表达式拒绝服务 (ReDoS)
- **风险**: 恶意正则表达式导致应用挂起
- **影响**: 服务不可用
- **修复**: 限制正则执行时间，使用验证库

#### 3. 路径遍历 (Path Traversal)
- **风险**: 访问预期之外的文件
- **影响**: 信息泄露、代码执行
- **修复**: 验证和规范化文件路径

#### 4. 依赖混淆 (Dependency Confusion)
- **风险**: 安装恶意包而非官方包
- **影响**: 供应链攻击
- **修复**: 使用 npm publish 2FA，锁定包范围

## Python 依赖安全

### 检查工具

#### 1. Safety
```bash
# 安装
pip install safety

# 扫描 requirements.txt
safety check -r requirements.txt

# 扫描漏到的虚拟环境
safety check

# JSON 输出
safety check --json > safety-report.json
```

#### 2. pip-audit
```bash
# 安装
pip install pip-audit

# 扫描
pip-audit

# 检查本地虚拟环境
pip-audit -v venv/
```

#### 3. Bandit - 静态分析
```bash
# 安装
pip install bandit

# 扫描 Python 代码
bandit -r ./myproject

# 生成报告
bandit -r ./myproject -f json -o report.json
```

### 依赖锁定

#### 使用 requirements.txt
```
Django==3.2.18  # 锁定具体版本
requests==2.27.1
numpy cryptography>=3.3,<4.0  # 范围约束
```

#### 使用 pip-tools
```bash
# 生成
pip-compile requirements.in --output-file requirements.txt

# 更新
pip-compile requirements.in --upgrade

# 检查不一致
pip-sync requirements.txt
```

#### 使用 poetry（现代推荐）
```bash
# 锁定依赖
poetry lock

# 安装精确版本
poetry install

# 更新
poetry update package-name
```

## 供应链安全最佳实践

### ✅ 推荐实践

1. **使用锁定文件**
   - `package-lock.json` (NPM)
   - `requirements.txt` (Python)
   - `poetry.lock` (Poetry)

2. **启用双因子认证 (2FA)**
   ```bash
   npm profile enable-2fa
   ```

3. **定期更新依赖**
   ```bash
   # 每周检查更新
   npm outdated
   pip list --outdated
   ```

4. **审查新依赖**
   - 检查包维护者
   - 查看包历史记录
   - 检查下载量（太少可能可疑）

5. **使用私有注册表**
   ```bash
   # 配置企业私有 NPM
   npm config set registry https://npm.example.com
   ```

### ❌ 避免风险

1. ❌ 忽略 `npm audit` 警告
2. ❌ 使用 `npm install` 而非 `npm ci` (生产)
3. ❌ 提交 `node_modules` 到版本控制
4. ❌ 在 CI 中使用 `npm install`@latest
5. ❌ 从不可信源安装包

## CI/CD 集成

### GitHub Actions 示例
```yaml
name: Security Audit

on: [push, pull_request]

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Run npm audit
        run: npm audit --audit-level=moderate
      - name: Fail on high/critical
        run: |
          if npm audit --json | jq '.metadata.vulnerabilities | .high + .critical' > 0; then
            echo "发现高危漏洞！"
            exit 1
          fi
```

### GitLab CI 示例
```yaml
security_audit:
  stage: security
  script:
    - npm audit --audit-level=high
    - pip install safety && safety check
  allow_failure: false
```

## 漏洞数据库参考

- **NPM Advisory Database**: https://github.com/npm/advisories
- **PyPI Security**: https://pypi.org/security
- **CVE Database**: https://cve.mitre.org/
- **OWASP Top 10**: https://owasp.org/www-project-top-ten/

## 自动修复策略

### 保守策略（推荐）
1. 获取审计报告
2. 人工审查修复方案
3. 测试每个修复
4. 逐步应用修复
5. 回归测试

### 激进策略（谨慎）
```bash
# 自动修复所有中危以上漏洞
npm audit fix --audit-level=moderate --force
```

### 回滚计划
```bash
# 修复前提交
git commit -am "Before audit fix"

# 应用修复
npm audit fix

# 测试失败时回滚
npm audit fix --force
git checkout HEAD~1 -- node_modules
```

## 监控和告警

### 设置自动扫描
```bash
# Cron 任务 - 每天审计
0 0 * * * npm audit --json > /var/log/npm-audit.log

# 检查告警
if grep -q '"high":1\|"critical":1' /var/log/npm-audit.log; then
  send_alert "发现高危漏洞"
fi
```


path: references/permissions.md
content: # 权限检查规则

## 文件权限标准

### 敏感文件最小权限
| 文件类型 | 推荐权限 | 命令 | 说明 |
|-----------|-----------|-------|------|
| SSH 私钥 | 600 | `chmod 600 ~/.ssh/id_rsa` | 仅所有者可读写 |
| SSH 目录 | 700 | `chmod 700 ~/.ssh` | 仅所有者可访问 |
| AWS 凭据 | 600 | `chmod 600 ~/.aws/credentials` | 保护 AWS 密钥 |
| GPG 目录 | 700 | `chmod 700 ~/.gnupg` | 保护 GPG 密钥 |
| OpenClaw 配置 | 600/700 | `chmod 700 ~/.openclaw` | 保护配置 |
| 日志文件 | 600/644 | `chmod 600 logs/*.log` | 控制日志访问 |

### 权限检查命令

```bash
# 查看文件权限
ls -la ~/.ssh

# 查找权限过宽松的文件
find ~ -perm -o+rwx -type f 2>/dev/null | head -20

# 修复 SSH 密钥权限
chmod 600 ~/.ssh/id_rsa
chmod 644 ~/.ssh/id_rsa.pub

# 修复 OpenClaw 目录权限
chmod 700 ~/.openclaw
chmod 600 ~/.openclaw/openclaw.json
```

### 常见权限问题

#### 问题 1: SSH 密钥权限过宽
- **症状**: SSH 连接提示 "Permissions 0644 are too open"
- **风险**: 其他用户可读取私钥
- **修复**: `chmod 600 ~/.ssh/id_*`

#### 问题 2: 配置文件全局可读
- **症状**: 敏感信息可能被其他用户访问
- **风险**: 信息泄露
- **修复**: `chmod 600 ~/.config/file.conf`

#### 问题 3: 日志文件权限不当
- **症状**: 日志可能包含敏感信息且可被任意用户读取
- **风险**: 信息泄露
- **修复**: `chmod 640 /var/log/app.log` (所有者读写，组只读)


path: references/secrets-detection.md
content: # 密钥检测规则

## 正则模式匹配

安全审计工具使用以下正则表达式检测各类密钥和凭据：

### 平台特定密钥

| 类型 | 正则模式 | 示例 |
|------|---------|------|
| AWS Access Key | `AKIA[0-9A-Z]{16}` | `[REDACTED_SECRET]` |
| GitHub Token (classic) | `ghp_[a-zA-Z0-9]{36}` | `[REDACTED_SECRET]` |
| GitHub Token (fine-grained) | `github_pat_[a-zA-Z0-9_]{22,}` | `github_pat_xxxxxxxxx...` |
| Slack Token | `xox[bpas]-[a-zA-Z0-9\-]+` | `xoxb-123-456-abc` |
| OpenAI / Anthropic Key | `sk-[a-zA-Z0-9]{20,}` | `[REDACTED_SECRET]` |
| JWT Token | `eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+` | `eyJhbGci...eyJzdWIi...` |
| Private Key Block | `-----BEGIN (RSA \|EC )?PRIVATE KEY-----` | PEM 格式私钥 |
| Generic API Key 赋值 | `(api_key\|api_secret\|...) = "..."` | 代码中的硬编码赋值 |

### 高熵字符串检测

对于无法匹配已知模式的密钥，使用 Shannon 信息熵检测：
- 阈值: entropy > 4.5
- 目标: 引号内长度 >= 20 的字母数字字符串
- 原理: 随机生成的密钥通常具有高熵值（> 4.5），而普通文本熵值较低（< 4.0）

```python
import math

def shannon_entropy(s: str) -> float:
    if not s:
        return 0.0
    freq = {}
    for c in s:
        freq[c] = freq.get(c, 0) + 1
    length = len(s)
    return -sum((count / length) * math.log2(count / length)
                for count in freq.values())
```

常见熵值参考：
- `"hello world"` → ~2.8
- `"password123"` → ~3.3
- 真实 API key → ~4.5 - 5.8
- UUID → ~3.7

## 敏感关键词列表

以下关键词用于环境变量和配置扫描（不区分大小写）：

### 认证凭据类
- password, passwd, pwd
- secret, secrets
- token, tokens
- key, keys, api_key, api-key, apikey
- auth, authorization
- credential, credentials
- private_key, private-key
- access_token, access-token
- client_secret, client-secret
- session_token, session-key
- refresh_token

### 平台特定
- AWS: aws_access_key, aws_secret_key, aws_session_token
- GitHub: github_token, github_pat
- GitLab: gitlab_token, gitlab_pat
- Slack: slack_token, slack_webhook
- Telegram: telegram_bot_token
- OpenAI: openai_api_key, sk-
- Anthropic: anthropic_api_key
- Feishu: feishu_app_secret, app_secret

## 环境变量检测

扫描当前进程环境变量中的敏感信息模式：

| 模式 | 匹配说明 |
|------|---------|
| `API[_-]?KEY\|API[_-]?SECRET` | API 密钥 |
| `ACCESS[_-]?TOKEN\|AUTH[_-]?TOKEN\|BEARER` | 访问令牌 |
| `PASSWORD\|PASSWD\|PWD` (排除 PATH) | 密码 |
| `SECRET[_-]?KEY\|PRIVATE[_-]?KEY` | 私钥 |
| `DB[_-]?PASSWORD\|DATABASE[_-]?URL` | 数据库凭据 |

## Shell 配置检测

扫描 `.bashrc` / `.zshrc` 等文件中的明文导出：

```
export\s+\w*(KEY|SECRET|TOKEN|PASSWORD|PASSWD)\w*\s*=\s*['"]?[a-zA-Z0-9/+=_\-]{8,}['"]?
```

## Git 历史检测

扫描最近 10 个 commit 的 diff 中是否包含上述密钥模式。

## 误报处理

以下情况应标记为误报：

### 1. 文档说明
- "如何设置 API_KEY 环境变量"
- "请使用 YOUR_SECRET_TOKEN"

### 2. 示例代码
```javascript
// 这是一个示例，请替换为实际密钥
const [REDACTED_SECRET]"
```

### 3. 测试数据
```json
{
  "test_password": "test123",
  "mock_token": "mock-token-value"
}
```

## 最佳实践

### ✅ 推荐做法

1. **使用环境变量**
```bash
export [REDACTED_SECRET]"
node app.js  # 代码中读取 process.env.API_KEY
```

2. **使用密钥管理服务**
```bash
# 使用 AWS Secrets Manager
aws secretsmanager get-secret-value --secret-id MySecret

# 使用 HashiCorp Vault
vault kv get secret/production/api-key
```

3. **配置文件分离**
```
.gitignore
---------
.env
.env.local
config/secrets.json
```

4. **使用掩码显示**
```json
{
  "apiKey": "__OPENCLAW_REDACTED__",
  "token": "sk-...xxxx"
}
```

### ❌ 避免做法

1. ❌ 硬编码密钥到代码库
2. ❌ 在配置文件中使用明文
3. ❌ 在日志中打印敏感信息
4. ❌ 将密钥提交到版本控制
5. ❌ 在公开代码中注释密钥
6. ❌ 在 .bashrc/.zshrc 中 export 密钥

## 检测后处理

发现敏感信息后的处理流程：

1. **立即标记严重性**
   - 生产环境密钥泄露 🔴 严重
   - 测试/开发密钥 🟡 中等
   - 示例/文档说明 🟢 低

2. **提供修复建议**
   - 移除敏感信息
   - 使用环境变量
   - 提交到 .env.example

3. **检查泄露历史**
   ```bash
   # 检查 Git 历史是否包含密钥
   git log --all --full-history -S "api_key" --source
   ```

4. **轮换凭证**
   - 撤销泄露的密钥
   - 生成新密钥
   - 更新所有使用位置


path: scripts/security_audit.py
content: #!/usr/bin/env python3
"""
Security Audit - 全方位安全审计脚本
检查 OpenClaw 配置、Skills、文件权限、依赖漏洞、环境变量、Git 安全、
网络端口、Shell 安全、macOS 安全等多维安全问题。

支持 CLI 参数、JSON 输出、配置文件、检查模块注册机制。
"""

import os
import sys
import json
import re
import math
import argparse
import subprocess
import platform
import datetime
from pathlib import Path
from typing import List, Dict, Any, Optional, Callable


# ==================== 颜色输出 ====================

class Colors:
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    ENDC = '\033[0m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'


def print_success(msg: str):
    print(f"{Colors.OKGREEN}✅ {msg}{Colors.ENDC}")


def print_warning(msg: str):
    print(f"{Colors.WARNING}⚠️  {msg}{Colors.ENDC}")


def print_error(msg: str):
    print(f"{Colors.FAIL}❌ {msg}{Colors.ENDC}")


def print_info(msg: str):
    print(f"{Colors.OKCYAN}ℹ️  {msg}{Colors.ENDC}")


def print_header(msg: str):
    print(f"\n{Colors.BOLD}{Colors.HEADER}{msg}{Colors.ENDC}")


# ==================== 配置 ====================

OPENCLAW_DIR = Path.home() / ".openclaw"
WORKSPACE_DIR = OPENCLAW_DIR / "workspace"
SKILLS_DIR = WORKSPACE_DIR / "skills"
CONFIG_FILE = OPENCLAW_DIR / "openclaw.json"


# ==================== 数据模型 ====================

class AuditResult:
    def __init__(self, category: str, name: str, status: str,
                 severity: str, description: str, impact: str, fix: str):
        self.category = category
        self.name = name
        self.status = status      # 'pass', 'warning', 'fail'
        self.severity = severity   # 'low', 'medium', 'high', 'critical'
        self.description = description
        self.impact = impact
        self.fix = fix
        self.timestamp = datetime.datetime.now().isoformat()

    def to_dict(self) -> Dict[str, Any]:
        return {
            "category": self.category,
            "name": self.name,
            "status": self.status,
            "severity": self.severity,
            "description": self.description,
            "impact": self.impact,
            "fix": self.fix,
            "timestamp": self.timestamp,
        }


# ==================== 检查模块注册 ====================

_CHECK_REGISTRY: Dict[str, Dict[str, Any]] = {}


def register_check(name: str, description: str = ""):
    """装饰器：注册一个检查模块"""
    def decorator(func: Callable[[], List[AuditResult]]):
        _CHECK_REGISTRY[name] = {
            "func": func,
            "description": description,
            "name": name,
        }
        return func
    return decorator


def get_registered_checks() -> Dict[str, Dict[str, Any]]:
    return _CHECK_REGISTRY


# ==================== 配置文件支持 ====================

def load_audit_config() -> Dict[str, Any]:
    """加载 .security-audit.json 配置文件"""
    default_config = {
        "excludePaths": [],
        "severityThreshold": "low",
        "autoFix": False,
        "reportFormat": "markdown",
    }

    config_locations = [
        Path.cwd() / ".security-audit.json",
        OPENCLAW_DIR / ".security-audit.json",
        WORKSPACE_DIR / ".security-audit.json",
    ]

    for config_path in config_locations:
        if config_path.exists():
            try:
                with open(config_path, 'r', encoding='utf-8') as f:
                    user_config = json.load(f)
                default_config.update(user_config)
                print_info(f"已加载配置文件: {config_path}")
                return default_config
            except (json.JSONDecodeError, OSError) as e:
                print_warning(f"配置文件解析失败: {config_path}: {e}")

    return default_config


# ==================== 密钥正则模式 ====================

SECRET_PATTERNS = {
    "AWS Access Key": re.compile(r'AKIA[0-9A-Z]{16}'),
    "GitHub Token (classic)": re.compile(r'ghp_[a-zA-Z0-9]{36}'),
    "GitHub Token (fine-grained)": re.compile(r'github_pat_[a-zA-Z0-9_]{22,}'),
    "Slack Token": re.compile(r'xox[bpas]-[a-zA-Z0-9\-]+'),
    "OpenAI/Anthropic Key": re.compile(r'sk-[a-zA-Z0-9]{20,}'),
    "JWT Token": re.compile(r'eyJ[a-zA-Z0-9_-]+\.eyJ[a-zA-Z0-9_-]+'),
    "Generic API Key assignment": re.compile(
        r'''(?:api[_-]?key|api[_-]?secret|access[_-]?token|auth[_-]?token)'''
        r'''[\s]*[=:]\s*['"][a-zA-Z0-9/+=_\-]{16,}['"]''',
        re.IGNORECASE,
    ),
    "Private Key Block": re.compile(r'-----BEGIN (?:RSA |EC )?PRIVATE KEY-----'),
}


def shannon_entropy(s: str) -> float:
    """计算字符串的 Shannon 熵"""
    if not s:
        return 0.0
    freq = {}
    for c in s:
        freq[c] = freq.get(c, 0) + 1
    length = len(s)
    return -sum((count / length) * math.log2(count / length) for count in freq.values())


# ==================== 检查模块 ====================

@register_check("permissions", "检查敏感文件权限")
def check_file_permissions() -> List[AuditResult]:
    """检查敏感文件权限"""
    results = []
    print_header("🔒 检查敏感文件权限")

    sensitive_files = [
        (Path.home() / ".ssh", 0o700),
        (Path.home() / ".ssh" / "id_rsa", 0o600),
        (Path.home() / ".ssh" / "id_ed25519", 0o600),
        (Path.home() / ".aws" / "credentials", 0o600),
        (Path.home() / ".gnupg", 0o700),
        (OPENCLAW_DIR, 0o700),
        (WORKSPACE_DIR, 0o700),
    ]

    for path, expected_mode in sensitive_files:
        if not path.exists():
            continue

        actual_mode = path.stat().st_mode & 0o777
        # BUG FIX: 原逻辑 `actual_mode < expected_mode` 反了。
        # 权限数值越大越宽松（0o777 > 0o700），应检查是否有超出预期的权限位。
        if actual_mode & ~expected_mode:
            results.append(AuditResult(
                category="权限与访问控制",
                name=f"敏感文件权限: {path.name}",
                status="fail",
                severity="high",
                description=f"文件权限过于宽松: {oct(actual_mode)}, 期望: {oct(expected_mode)}",
                impact=str(path),
                fix=f"chmod {oct(expected_mode)[2:]} {path}",
            ))
        else:
            results.append(AuditResult(
                category="权限与访问控制",
                name=f"敏感文件权限: {path.name}",
                status="pass",
                severity="low",
                description="文件权限设置正确",
                impact=str(path),
                fix="无需操作",
            ))

    return results


@register_check("world_writable", "检测世界可写及 SUID/SGID 文件")
def check_world_writable() -> List[AuditResult]:
    """检测 workspace 下世界可写文件和 SUID/SGID 文件"""
    results = []
    print_header("🔓 检测世界可写文件 / SUID/SGID 文件")

    scan_dir = WORKSPACE_DIR if WORKSPACE_DIR.exists() else Path.cwd()

    world_writable = []
    suid_sgid = []

    try:
        for path in scan_dir.rglob("*"):
            if not path.exists() or path.is_symlink():
                continue
            try:
                mode = path.stat().st_mode
            except OSError:
                continue

            if mode & 0o002:  # o+w
                world_writable.append(path)
            if mode & 0o6000:  # SUID or SGID
                suid_sgid.append((path, mode))
    except PermissionError:
        pass

    if world_writable:
        paths_str = "\n".join(str(p) for p in world_writable[:10])
        suffix = f"\n... 及其他 {len(world_writable) - 10} 个" if len(world_writable) > 10 else ""
        results.append(AuditResult(
            category="权限与访问控制",
            name="世界可写文件",
            status="fail",
            severity="high",
            description=f"发现 {len(world_writable)} 个世界可写文件",
            impact=f"{paths_str}{suffix}",
            fix="chmod o-w <file> 移除世界可写权限",
        ))
    else:
        results.append(AuditResult(
            category="权限与访问控制",
            name="世界可写文件",
            status="pass",
            severity="low",
            description="未发现世界可写文件",
            impact=str(scan_dir),
            fix="无需操作",
        ))

    if suid_sgid:
        paths_str = "\n".join(f"{p} ({oct(m & 0o7777)})" for p, m in suid_sgid[:10])
        results.append(AuditResult(
            category="权限与访问控制",
            name="SUID/SGID 文件",
            status="warning",
            severity="medium",
            description=f"发现 {len(suid_sgid)} 个 SUID/SGID 文件",
            impact=paths_str,
            fix="审查 SUID/SGID 设置的必要性，使用 chmod u-s,g-s <file> 移除",
        ))

    return results


@register_check("config", "检查 OpenClaw 配置文件安全性")
def check_openclaw_config() -> List[AuditResult]:
    """检查 OpenClaw 配置文件安全性"""
    results = []
    print_header("🔐 检查 OpenClaw 配置")

    if not CONFIG_FILE.exists():
        return [AuditResult(
            category="密钥与凭据安全",
            name="OpenClaw 配置文件",
            status="warning",
            severity="medium",
            description="配置文件不存在",
            impact="无法检查配置",
            fix="请检查 OpenClaw 安装",
        )]

    try:
        with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
            config = json.load(f)

        config_str = json.dumps(config, indent=2)

        # 用正则模式扫描配置中的真实密钥
        found_secrets = []
        for pattern_name, pattern in SECRET_PATTERNS.items():
            if pattern.search(config_str):
                found_secrets.append(pattern_name)

        if found_secrets:
            results.append(AuditResult(
                category="密钥与凭据安全",
                name="配置文件密钥泄露",
                status="fail",
                severity="critical",
                description=f"配置文件中检测到真实密钥模式: {', '.join(found_secrets)}",
                impact=str(CONFIG_FILE),
                fix="将密钥移至环境变量，配置文件中使用掩码占位符",
            ))

        # 检查是否有掩码
        if "__OPENCLAW_REDACTED__" not in config_str and not found_secrets:
            results.append(AuditResult(
                category="密钥与凭据安全",
                name="配置密钥掩码",
                status="warning",
                severity="medium",
                description="配置文件中未发现掩码标记，请确认敏感信息已处理",
                impact=str(CONFIG_FILE),
                fix="检查配置文件，确保敏感信息已通过掩码或环境变量处理",
            ))
        elif not found_secrets:
            results.append(AuditResult(
                category="密钥与凭据安全",
                name="配置密钥掩码",
                status="pass",
                severity="low",
                description="敏感信息已正确掩码",
                impact=str(CONFIG_FILE),
                fix="无需操作",
            ))

        # 检查 gateway 认证
        if 'gateway' in config:
            if config.get('gateway', {}).get('auth', {}).get('mode') == 'token':
                results.append(AuditResult(
                    category="密钥与凭据安全",
                    name="Gateway 认证方式",
                    status="warning",
                    severity="medium",
                    description="Gateway 使用 token 认证，请确保 token 安全存储",
                    impact="Gateway 访问",
                    fix="考虑使用更安全的认证方式或定期轮换 token",
                ))

        # 检查插件配置
        if 'plugins' in config and 'installs' in config['plugins']:
            for plugin_name, plugin_info in config['plugins']['installs'].items():
                if 'installPath' in plugin_info:
                    results.append(AuditResult(
                        category="系统安全",
                        name=f"插件安装: {plugin_name}",
                        status="pass",
                        severity="low",
                        description=f"插件已安装: {plugin_info.get('version', 'unknown')}",
                        impact=str(plugin_info.get('installPath', '')),
                        fix="定期更新插件版本",
                    ))

    except Exception as e:
        results.append(AuditResult(
            category="密钥与凭据安全",
            name="OpenClaw 配置解析",
            status="fail",
            severity="critical",
            description=f"配置文件解析失败: {str(e)}",
            impact=str(CONFIG_FILE),
            fix="检查配置文件 JSON 格式是否正确",
        ))

    return results


@register_check("skills", "检查 Skills 代码安全")
def check_skills_security() -> List[AuditResult]:
    """检查 Skills 安全性"""
    results = []
    print_header("📝 检查 Skills 代码安全")

    if not SKILLS_DIR.exists():
        return [AuditResult(
            category="代码安全",
            name="Skills 目录",
            status="warning",
            severity="medium",
            description="Skills 目录不存在",
            impact="无",
            fix="确保 Skills 目录正确配置",
        )]

    dangerous_functions = [
        'eval(', 'exec(', '__import__', 'subprocess.call(',
        'os.system(', 'os.popen(', 'subprocess.run(',
    ]

    for skill_dir in SKILLS_DIR.iterdir():
        if not skill_dir.is_dir():
            continue

        skill_file = skill_dir / "SKILL.md"
        if not skill_file.exists():
            continue

        try:
            with open(skill_file, 'r', encoding='utf-8') as f:
                content = f.read()

            # 用正则检测硬编码密钥
            found_secrets = []
            for pattern_name, pattern in SECRET_PATTERNS.items():
                if pattern.search(content):
                    found_secrets.append(pattern_name)

            if found_secrets:
                results.append(AuditResult(
                    category="密钥与凭据安全",
                    name=f"Skills 硬编码密钥: {skill_dir.name}",
                    status="fail",
                    severity="high",
                    description=f"发现密钥模式: {', '.join(found_secrets[:3])}",
                    impact=str(skill_file),
                    fix="将敏感信息移至环境变量或配置文件",
                ))

            # 检查危险函数
            content_lower = content.lower()
            found_dangerous = [fn for fn in dangerous_functions if fn in content_lower]
            if found_dangerous:
                results.append(AuditResult(
                    category="代码安全",
                    name=f"Skills 危险函数: {skill_dir.name}",
                    status="warning",
                    severity="medium",
                    description=f"发现可能的危险函数: {', '.join(found_dangerous[:3])}",
                    impact=str(skill_file),
                    fix="审查代码逻辑，确保不执行未经验证的输入",
                ))

            # 检查脚本文件
            scripts_dir = skill_dir / "scripts"
            if scripts_dir.exists():
                for script_file in scripts_dir.glob("*.py"):
                    _check_script_file(script_file, results)

        except Exception as e:
            results.append(AuditResult(
                category="代码安全",
                name=f"Skills 检查: {skill_dir.name}",
                status="warning",
                severity="low",
                description=f"检查失败: {str(e)}",
                impact=str(skill_dir),
                fix="检查文件编码和权限",
            ))

    return results


def _check_script_file(script_file: Path, results: List[AuditResult]):
    """检查单个脚本文件中的密钥模式"""
    try:
        with open(script_file, 'r', encoding='utf-8') as f:
            content = f.read()

        for pattern_name, pattern in SECRET_PATTERNS.items():
            if pattern.search(content):
                results.append(AuditResult(
                    category="密钥与凭据安全",
                    name=f"脚本密钥泄露: {script_file.name}",
                    status="fail",
                    severity="high",
                    description=f"检测到 {pattern_name} 模式",
                    impact=str(script_file),
                    fix="使用环境变量或配置文件管理密钥",
                ))
                break

        # 高熵字符串检测
        # 查找长引号字符串中的高熵内容
        for match in re.finditer(r'''['"]([a-zA-Z0-9/+=_\-]{20,})['"]''', content):
            [REDACTED_SECRET])
            if shannon_entropy(token) > 4.5:
                results.append(AuditResult(
                    category="密钥与凭据安全",
                    name=f"脚本高熵字符串: {script_file.name}",
                    status="warning",
                    severity="medium",
                    description=f"发现高熵字符串 (entropy={shannon_entropy(token):.1f}): {token[:16]}...",
                    impact=str(script_file),
                    fix="确认此字符串是否为密钥，如是请移至环境变量",
                ))
                break

    except Exception:
        pass


@register_check("dependencies", "检查依赖漏洞")
def check_dependencies() -> List[AuditResult]:
    """检查依赖漏洞"""
    results = []
    print_header("📦 检查依赖漏洞")

    package_jsons = list(WORKSPACE_DIR.rglob("package.json")) if WORKSPACE_DIR.exists() else []

    for pkg_file in package_jsons:
        try:
            dir_path = pkg_file.parent

            if not (dir_path / "node_modules").exists():
                results.append(AuditResult(
                    category="依赖与供应链安全",
                    name=f"NPM 依赖未安装: {dir_path.name}",
                    status="info",
                    severity="low",
                    description="需要运行 npm install",
                    impact=str(dir_path),
                    fix=f"cd {dir_path} && npm install",
                ))
                continue

            result = subprocess.run(
                ['npm', 'audit', '--json'],
                cwd=dir_path,
                capture_output=True,
                text=True,
                timeout=60,
            )

            # BUG FIX: npm audit 发现漏洞时返回非零 returncode。
            # 无论 returncode 如何都应解析 JSON stdout。
            try:
                audit_data = json.loads(result.stdout)
                vulnerabilities = audit_data.get('vulnerabilities', {})

                if vulnerabilities:
                    total_vulns = len(vulnerabilities)
                    severity_counts = {}
                    for vuln_info in vulnerabilities.values():
                        sev = vuln_info.get('severity', 'unknown')
                        severity_counts[sev] = severity_counts.get(sev, 0) + 1

                    severity_str = ", ".join(f"{k}: {v}" for k, v in severity_counts.items())
                    max_sev = "critical" if "critical" in severity_counts else (
                        "high" if "high" in severity_counts else "medium"
                    )

                    results.append(AuditResult(
                        category="依赖与供应链安全",
                        name=f"NPM 漏洞: {dir_path.name}",
                        status="fail",
                        severity=max_sev,
                        description=f"发现 {total_vulns} 个已知漏洞 ({severity_str})",
                        impact=str(pkg_file),
                        fix=f"cd {dir_path} && npm audit fix",
                    ))
                else:
                    results.append(AuditResult(
                        category="依赖与供应链安全",
                        name=f"NPM 漏洞: {dir_path.name}",
                        status="pass",
                        severity="low",
                        description="无已知漏洞",
                        impact=str(pkg_file),
                        fix="定期运行 npm audit",
                    ))
            except json.JSONDecodeError:
                results.append(AuditResult(
                    category="依赖与供应链安全",
                    name=f"NPM 审计: {dir_path.name}",
                    status="warning",
                    severity="low",
                    description=f"npm audit 输出格式异常 (returncode={result.returncode})",
                    impact=str(pkg_file),
                    fix="检查 npm 版本",
                ))

        except subprocess.TimeoutExpired:
            results.append(AuditResult(
                category="依赖与供应链安全",
                name=f"NPM 审计超时: {dir_path.name}",
                status="warning",
                severity="low",
                description="npm audit 执行超时",
                impact=str(pkg_file),
                fix="稍后重试或手动检查",
            ))
        except FileNotFoundError:
            results.append(AuditResult(
                category="依赖与供应链安全",
                name="NPM 未安装",
                status="warning",
                severity="low",
                description="系统未安装 npm",
                impact="NPM 依赖审计不可用",
                fix="安装 Node.js / npm",
            ))
            break
        except Exception as e:
            results.append(AuditResult(
                category="依赖与供应链安全",
                name=f"NPM 审计: {dir_path.name}",
                status="warning",
                severity="low",
                description=f"检查失败: {str(e)}",
                impact=str(pkg_file),
                fix="检查 npm 是否正确安装",
            ))

    return results


@register_check("env", "扫描环境变量中的敏感信息")
def check_env_variables() -> List[AuditResult]:
    """扫描当前进程环境变量中的敏感信息"""
    results = []
    print_header("🌍 检查环境变量安全")

    sensitive_env_patterns = [
        (re.compile(r'(?:API[_-]?KEY|API[_-]?SECRET)', re.I), "API 密钥"),
        (re.compile(r'(?:ACCESS[_-]?TOKEN|AUTH[_-]?TOKEN|BEARER)', re.I), "访问令牌"),
        (re.compile(r'(?:PASSWORD|PASSWD|PWD)(?!.*PATH)', re.I), "密码"),
        (re.compile(r'(?:SECRET[_-]?KEY|PRIVATE[_-]?KEY)', re.I), "私钥"),
        (re.compile(r'(?:DB[_-]?PASSWORD|DATABASE[_-]?URL)', re.I), "数据库凭据"),
    ]

    exposed = []

    for env_name, env_value in os.environ.items():
        for pattern, desc in sensitive_env_patterns:
            if pattern.search(env_name):
                # 检查值是否是真实密钥（非占位符）
                if env_value and env_value not in ('', 'your-key-here', 'changeme', 'xxx'):
                    exposed.append((env_name, desc))
                break

        # 检查值中是否包含已知密钥格式
        if env_value:
            for pattern_name, pattern in SECRET_PATTERNS.items():
                if pattern.search(env_value):
                    exposed.append((env_name, f"值匹配 {pattern_name} 模式"))
                    break

    if exposed:
        details = "\n".join(f"  - {name}: {desc}" for name, desc in exposed[:10])
        results.append(AuditResult(
            category="密钥与凭据安全",
            name="环境变量敏感信息",
            status="warning",
            severity="medium",
            description=f"发现 {len(exposed)} 个可能含敏感信息的环境变量:\n{details}",
            impact="当前进程环境",
            fix="审查这些环境变量是否必要，避免在不需要的子进程中暴露",
        ))
    else:
        results.append(AuditResult(
            category="密钥与凭据安全",
            name="环境变量敏感信息",
            status="pass",
            severity="low",
            description="未发现异常敏感环境变量",
            impact="当前进程环境",
            fix="无需操作",
        ))

    return results


@register_check("git", "检查 Git 安全配置")
def check_git_security() -> List[AuditResult]:
    """检查 Git 安全：.gitignore、凭据存储、近期 commit 中的密钥"""
    results = []
    print_header("🔀 检查 Git 安全")

    # 查找 workspace 下的 git 仓库
    git_repos = []
    scan_dir = WORKSPACE_DIR if WORKSPACE_DIR.exists() else Path.cwd()

    for gitdir in scan_dir.rglob(".git"):
        if gitdir.is_dir():
            git_repos.append(gitdir.parent)

    if not git_repos:
        results.append(AuditResult(
            category="代码安全",
            name="Git 仓库",
            status="pass",
            severity="low",
            description="未发现 Git 仓库",
            impact=str(scan_dir),
            fix="无需操作",
        ))
        return results

    sensitive_gitignore_entries = [
        '.env', '.env.local', '.env.production',
        '*.pem', '*.key', '*.p12',
        'credentials.json', 'secrets.json', 'service-account.json',
        '.secret', '*.secret',
    ]

    for repo in git_repos[:5]:  # 限制扫描仓库数量
        # 检查 .gitignore
        gitignore = repo / ".gitignore"
        if gitignore.exists():
            try:
                with open(gitignore, 'r', encoding='utf-8') as f:
                    gitignore_content = f.read()
                missing = [
                    entry for entry in sensitive_gitignore_entries
                    if entry not in gitignore_content
                ]
                if missing:
                    results.append(AuditResult(
                        category="代码安全",
                        name=f"Git .gitignore 缺失条目: {repo.name}",
                        status="warning",
                        severity="medium",
                        description=f".gitignore 未包含: {', '.join(missing[:5])}",
                        impact=str(gitignore),
                        fix=f"将以下条目添加到 .gitignore: {', '.join(missing[:5])}",
                    ))
            except OSError:
                pass
        else:
            results.append(AuditResult(
                category="代码安全",
                name=f"Git .gitignore 缺失: {repo.name}",
                status="warning",
                severity="medium",
                description="仓库缺少 .gitignore 文件",
                impact=str(repo),
                fix="创建 .gitignore 并添加敏感文件模式",
            ))

        # 检查 git config 凭据存储
        try:
            result = subprocess.run(
                ['git', 'config', '--local', 'credential.helper'],
                cwd=repo, capture_output=True, text=True, timeout=5,
            )
            helper = result.stdout.strip()
            if helper == 'store':
                results.append(AuditResult(
                    category="密钥与凭据安全",
                    name=f"Git 凭据明文存储: {repo.name}",
                    status="fail",
                    severity="high",
                    description="Git 配置使用 credential.helper=store (明文存储)",
                    impact=str(repo / ".git" / "config"),
                    fix="git config --local credential.helper osxkeychain  (macOS) 或 cache",
                ))
        except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
            pass

        # 扫描最近 commit 的 diff 中的密钥模式
        try:
            result = subprocess.run(
                ['git', 'log', '--oneline', '-10', '--diff-filter=A', '-p', '--'],
                cwd=repo, capture_output=True, text=True, timeout=15,
            )
            if result.stdout:
                for pattern_name, pattern in SECRET_PATTERNS.items():
                    if pattern.search(result.stdout):
                        results.append(AuditResult(
                            category="密钥与凭据安全",
                            name=f"Git 历史密钥泄露: {repo.name}",
                            status="fail",
                            severity="critical",
                            description=f"最近 commit 中检测到 {pattern_name} 模式",
                            impact=str(repo),
                            fix="使用 git-filter-repo 或 BFG 清理历史，并轮换泄露的密钥",
                        ))
                        break
        except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
            pass

    return results


@register_check("network", "检查网络监听端口")
def check_network_ports() -> List[AuditResult]:
    """使用 lsof 检查监听端口"""
    results = []
    print_header("🌐 检查网络端口安全")

    try:
        result = subprocess.run(
            ['lsof', '-i', '-P', '-n', '-sTCP:LISTEN'],
            capture_output=True, text=True, timeout=10,
        )

        if result.returncode != 0 and not result.stdout:
            results.append(AuditResult(
                category="网络安全",
                name="端口扫描",
                status="pass",
                severity="low",
                description="未发现监听端口（或需要 sudo 权限）",
                impact="网络监听",
                fix="无需操作",
            ))
            return results

        lines = result.stdout.strip().split('\n')
        if len(lines) <= 1:
            results.append(AuditResult(
                category="网络安全",
                name="端口扫描",
                status="pass",
                severity="low",
                description="未发现监听端口",
                impact="网络监听",
                fix="无需操作",
            ))
            return results

        wildcard_listeners = []
        all_listeners = []

        for line in lines[1:]:  # 跳过 header
            parts = line.split()
            if len(parts) < 9:
                continue

            process_name = parts[0]
            pid = parts[1]
            name_field = parts[8]  # host:port

            all_listeners.append(f"{process_name}(pid={pid}) -> {name_field}")

            # 检查 0.0.0.0 或 * 监听（全接口暴露）
            if name_field.startswith('*:') or name_field.startswith('0.0.0.0:'):
                wildcard_listeners.append(f"{process_name}(pid={pid}) -> {name_field}")

        if wildcard_listeners:
            details = "\n".join(f"  - {l}" for l in wildcard_listeners[:10])
            results.append(AuditResult(
                category="网络安全",
                name="全接口监听端口",
                status="warning",
                severity="high",
                description=f"发现 {len(wildcard_listeners)} 个进程监听所有网络接口:\n{details}",
                impact="外部可能直接访问这些服务",
                fix="将监听地址限制为 127.0.0.1 或特定接口",
            ))

        if all_listeners:
            details = "\n".join(f"  - {l}" for l in all_listeners[:15])
            results.append(AuditResult(
                category="网络安全",
                name="监听端口清单",
                status="pass" if not wildcard_listeners else "warning",
                severity="low",
                description=f"当前监听 {len(all_listeners)} 个端口:\n{details}",
                impact="网络监听",
                fix="审查是否所有端口都是必要的",
            ))

    except FileNotFoundError:
        results.append(AuditResult(
            category="网络安全",
            name="端口扫描",
            status="warning",
            severity="low",
            description="lsof 命令不可用",
            impact="网络端口检查不可用",
            fix="安装 lsof 或使用 netstat",
        ))
    except subprocess.TimeoutExpired:
        results.append(AuditResult(
            category="网络安全",
            name="端口扫描超时",
            status="warning",
            severity="low",
            description="lsof 执行超时",
            impact="网络端口检查",
            fix="稍后重试",
        ))

    return results


@register_check("shell", "检查 Shell 历史与配置安全")
def check_shell_security() -> List[AuditResult]:
    """检查 shell 历史文件权限和配置文件中的明文密钥"""
    results = []
    print_header("🐚 检查 Shell 安全")

    home = Path.home()

    # 检查历史文件权限
    history_files = [
        home / ".bash_history",
        home / ".zsh_history",
        home / ".python_history",
    ]

    for hist_file in history_files:
        if not hist_file.exists():
            continue
        mode = hist_file.stat().st_mode & 0o777
        if mode & ~0o600:
            results.append(AuditResult(
                category="权限与访问控制",
                name=f"Shell 历史文件权限: {hist_file.name}",
                status="warning",
                severity="medium",
                description=f"权限过于宽松: {oct(mode)}，可能泄露命令历史",
                impact=str(hist_file),
                fix=f"chmod 600 {hist_file}",
            ))

    # 检查 rc 文件中的明文导出密钥
    rc_files = [
        home / ".bashrc",
        home / ".zshrc",
        home / ".bash_profile",
        home / ".profile",
    ]

    export_secret_pattern = re.compile(
        r'''export\s+\w*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD)\w*\s*=\s*['"]?[a-zA-Z0-9/+=_\-]{8,}['"]?''',
        re.IGNORECASE,
    )

    for rc_file in rc_files:
        if not rc_file.exists():
            continue
        try:
            with open(rc_file, 'r', encoding='utf-8', errors='ignore') as f:
                content = f.read()

            matches = export_secret_pattern.findall(content)
            if matches:
                sanitized = [m[:50] + "..." if len(m) > 50 else m for m in matches[:3]]
                results.append(AuditResult(
                    category="密钥与凭据安全",
                    name=f"Shell 配置明文密钥: {rc_file.name}",
                    status="fail",
                    severity="high",
                    description=f"发现 {len(matches)} 处明文导出密钥: {'; '.join(sanitized)}",
                    impact=str(rc_file),
                    fix="将密钥移至 .env 文件或密钥管理服务，不在 shell 配置中硬编码",
                ))
        except OSError:
            pass

    if not any(r.status in ('fail', 'warning') for r in results):
        results.append(AuditResult(
            category="密钥与凭据安全",
            name="Shell 安全检查",
            status="pass",
            severity="low",
            description="Shell 配置未发现安全问题",
            impact="Shell 环境",
            fix="无需操作",
        ))

    return results


@register_check("macos", "检查 macOS 安全设置")
def check_macos_security() -> List[AuditResult]:
    """检查 macOS 特有的安全设置"""
    results = []

    if platform.system() != 'Darwin':
        return results

    print_header("🍎 检查 macOS 安全设置")

    # 检查防火墙
    try:
        result = subprocess.run(
            ['/usr/libexec/ApplicationFirewall/socketfilterfw', '--getglobalstate'],
            capture_output=True, text=True, timeout=5,
        )
        if 'enabled' in result.stdout.lower():
            results.append(AuditResult(
                category="系统安全",
                name="macOS 防火墙",
                status="pass",
                severity="low",
                description="防火墙已启用",
                impact="网络安全",
                fix="无需操作",
            ))
        else:
            results.append(AuditResult(
                category="系统安全",
                name="macOS 防火墙",
                status="fail",
                severity="high",
                description="防火墙未启用",
                impact="系统可能暴露不必要的网络服务",
                fix="系统偏好设置 > 安全性与隐私 > 防火墙 > 打开防火墙",
            ))
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        results.append(AuditResult(
            category="系统安全",
            name="macOS 防火墙",
            status="warning",
            severity="low",
            description="无法检查防火墙状态",
            impact="网络安全",
            fix="手动检查: /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate",
        ))

    # 检查 Gatekeeper
    try:
        result = subprocess.run(
            ['spctl', '--status'],
            capture_output=True, text=True, timeout=5,
        )
        output = result.stdout.strip() + result.stderr.strip()
        if 'enabled' in output.lower():
            results.append(AuditResult(
                category="系统安全",
                name="macOS Gatekeeper",
                status="pass",
                severity="low",
                description="Gatekeeper 已启用",
                impact="应用安全",
                fix="无需操作",
            ))
        else:
            results.append(AuditResult(
                category="系统安全",
                name="macOS Gatekeeper",
                status="fail",
                severity="high",
                description="Gatekeeper 已禁用，可能运行未签名应用",
                impact="系统可能运行恶意软件",
                fix="sudo spctl --master-enable",
            ))
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        pass

    # 检查 SIP
    try:
        result = subprocess.run(
            ['csrutil', 'status'],
            capture_output=True, text=True, timeout=5,
        )
        if 'enabled' in result.stdout.lower():
            results.append(AuditResult(
                category="系统安全",
                name="macOS SIP (系统完整性保护)",
                status="pass",
                severity="low",
                description="SIP 已启用",
                impact="系统完整性",
                fix="无需操作",
            ))
        else:
            results.append(AuditResult(
                category="系统安全",
                name="macOS SIP (系统完整性保护)",
                status="fail",
                severity="critical",
                description="SIP 已禁用，系统文件可能被篡改",
                impact="系统完整性保护缺失",
                fix="重启到恢复模式，运行 csrutil enable",
            ))
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        pass

    return results


# ==================== 报告生成 ====================

def generate_report(results: List[AuditResult]) -> str:
    """生成 Markdown 格式报告"""
    now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    total = len(results)
    passed = sum(1 for r in results if r.status == 'pass')
    warnings = sum(1 for r in results if r.status == 'warning')
    fails = sum(1 for r in results if r.status == 'fail')

    critical_count = sum(1 for r in results if r.severity == 'critical')
    high_count = sum(1 for r in results if r.severity == 'high')
    medium_count = sum(1 for r in results if r.severity == 'medium')
    low_count = sum(1 for r in results if r.severity == 'low')

    report = []
    report.append("# 🔒 OpenClaw 安全审计报告")
    report.append(f"\n**审计时间**: {now}")

    # 列出执行的检查模块
    categories = sorted(set(r.category for r in results))
    report.append(f"**审计范围**: {', '.join(categories)}")

    report.append("\n---\n## 📊 执行摘要")
    report.append(f"- ✅ 通过: {passed}")
    report.append(f"- ⚠️ 警告: {warnings}")
    report.append(f"- ❌ 失败: {fails}")
    report.append(f"- 📋 总计: {total}")

    report.append("\n### 风险等级分布")
    report.append(f"- 🔴 严重: {critical_count}")
    report.append(f"- 🟠 高: {high_count}")
    report.append(f"- 🟡 中: {medium_count}")
    report.append(f"- 🟢 低: {low_count}")

    severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3}
    sorted_results = sorted(results, key=lambda x: severity_order.get(x.severity, 4))

    report.append("\n---\n## 🔍 详细检查结果")

    current_category = None
    for result in sorted_results:
        if result.category != current_category:
            current_category = result.category
            report.append(f"\n### {result.category}")

        status_icon = {"pass": "✅", "warning": "⚠️", "fail": "❌"}.get(result.status, "ℹ️")
        severity_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🟢"}.get(result.severity, "⚪")

        report.append(f"\n{status_icon} **{result.name}**")
        report.append(f"- 状态: {result.status}")
        report.append(f"- 风险等级: {severity_icon} {result.severity}")

        if result.status != "pass":
            report.append(f"- 问题描述: {result.description}")
            report.append(f"- 影响范围: `{result.impact}`")
            report.append(f"- 修复建议: `{result.fix}`")

    failed_results = [r for r in sorted_results if r.status in ['fail', 'warning']]
    if failed_results:
        report.append("\n---\n## 📋 优先修复清单")

        for i, result in enumerate(failed_results, 1):
            severity_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🟢"}.get(result.severity, "⚪")
            report.append(f"\n{i}. {severity_icon} **{result.name}**")
            report.append(f"   ```bash")
            report.append(f"   {result.fix}")
            report.append(f"   ```")

    return "\n".join(report)


def generate_json_report(results: List[AuditResult]) -> str:
    """生成 JSON 格式报告"""
    now = datetime.datetime.now().isoformat()

    report = {
        "audit_time": now,
        "summary": {
            "total": len(results),
            "passed": sum(1 for r in results if r.status == 'pass'),
            "warnings": sum(1 for r in results if r.status == 'warning'),
            "failed": sum(1 for r in results if r.status == 'fail'),
            "by_severity": {
                "critical": sum(1 for r in results if r.severity == 'critical'),
                "high": sum(1 for r in results if r.severity == 'high'),
                "medium": sum(1 for r in results if r.severity == 'medium'),
                "low": sum(1 for r in results if r.severity == 'low'),
            },
        },
        "results": [r.to_dict() for r in results],
    }

    return json.dumps(report, indent=2, ensure_ascii=False)


def save_report(report: str, output_file: Optional[str] = None, extension: str = "md"):
    """保存报告到文件"""
    if output_file is None:
        timestamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
        output_file = OPENCLAW_DIR / "logs" / f"security-audit-{timestamp}.{extension}"

    output_path = Path(output_file)
    output_path.parent.mkdir(parents=True, exist_ok=True)

    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(report)

    print_success(f"报告已保存到: {output_path}")
    return str(output_path)


# ==================== CLI ====================

def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="OpenClaw 安全审计工具 - 全方位安全检查",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""\
示例:
  %(prog)s                          运行所有检查
  %(prog)s --check permissions env  只运行权限和环境变量检查
  %(prog)s --severity high          只显示 high 及以上级别
  %(prog)s --json                   输出 JSON 格式
  %(prog)s --json --output report.json
  %(prog)s --list-checks            列出可用检查模块
""",
    )

    parser.add_argument(
        '--check', nargs='+', metavar='NAME',
        help="指定要运行的检查模块（默认全部）",
    )
    parser.add_argument(
        '--severity', choices=['low', 'medium', 'high', 'critical'],
        default='low',
        help="最低显示/报告的严重级别 (默认: low)",
    )
    parser.add_argument(
        '--output', '-o', metavar='FILE',
        help="报告输出路径",
    )
    parser.add_argument(
        '--json', action='store_true', dest='json_output',
        help="输出 JSON 格式报告",
    )
    parser.add_argument(
        '--list-checks', action='store_true',
        help="列出所有可用的检查模块",
    )
    parser.add_argument(
        '--quiet', '-q', action='store_true',
        help="静默模式，只输出报告不输出进度信息",
    )

    return parser


def filter_by_severity(results: List[AuditResult], min_severity: str) -> List[AuditResult]:
    """按最低严重级别过滤结果"""
    severity_levels = {'low': 0, 'medium': 1, 'high': 2, 'critical': 3}
    threshold = severity_levels.get(min_severity, 0)
    return [r for r in results if severity_levels.get(r.severity, 0) >= threshold]


# ==================== 主程序 ====================

def main():
    parser = build_parser()
    args = parser.parse_args()

    checks = get_registered_checks()

    # 列出检查模块
    if args.list_checks:
        print(f"\n可用检查模块 ({len(checks)} 个):\n")
        for name, info in checks.items():
            print(f"  {name:20s} - {info['description']}")
        print(f"\n使用 --check <name> 指定运行的模块")
        return

    # 加载配置
    config = load_audit_config()

    if not args.quiet:
        print(f"{Colors.BOLD}{Colors.HEADER}🔒 OpenClaw 安全审计工具{Colors.ENDC}")
        print(f"开始时间: {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

    # 确定要运行的检查
    if args.check:
        selected = []
        for name in args.check:
            if name not in checks:
                print_error(f"未知检查模块: {name}")
                print_info(f"可用模块: {', '.join(checks.keys())}")
                sys.exit(1)
            selected.append(name)
    else:
        selected = list(checks.keys())

    # 运行检查
    all_results = []
    for name in selected:
        check_func = checks[name]["func"]
        try:
            results = check_func()
            all_results.extend(results)
        except Exception as e:
            print_error(f"检查模块 {name} 执行失败: {e}")
            all_results.append(AuditResult(
                category="系统",
                name=f"模块执行失败: {name}",
                status="warning",
                severity="low",
                description=str(e),
                impact=name,
                fix="检查模块代码",
            ))

    # 应用配置中的 severityThreshold
    config_severity = config.get("severityThreshold", "low")
    effective_severity = args.severity if args.severity != 'low' else config_severity
    filtered_results = filter_by_severity(all_results, effective_severity)

    # 生成报告
    if args.json_output:
        report = generate_json_report(filtered_results)
        extension = "json"
    else:
        report = generate_report(filtered_results)
        extension = "md"

    # 保存报告
    report_file = save_report(report, output_file=args.output, extension=extension)

    # 输出到控制台
    if not args.quiet:
        print("\n" + report)
        print_info(f"审计完成，共 {len(all_results)} 个检查项"
                   + (f"，显示 {len(filtered_results)} 个 (>= {effective_severity})"
                      if len(filtered_results) != len(all_results) else ""))

    return report_file


if __name__ == "__main__":
    main()


path: scripts/send_report_to_feishu.py
content: #!/usr/bin/env python3
"""
Send Security Audit Report to Feishu
将安全审计报告发送到飞书，支持富文本 (post) 消息格式和 OpenClaw 插件 API。
"""

import sys
import json
import os
from pathlib import Path
import urllib.request
import urllib.error


def print_success(msg: str):
    print(f"✅ {msg}")


def print_error(msg: str):
    print(f"❌ {msg}")


def print_info(msg: str):
    print(f"ℹ️  {msg}")


def load_feishu_config():
    """加载飞书配置"""
    openclaw_dir = Path.home() / ".openclaw"
    config_file = openclaw_dir / "openclaw.json"

    if not config_file.exists():
        print_error("OpenClaw 配置文件不存在")
        return None

    with open(config_file, 'r', encoding='utf-8') as f:
        config = json.load(f)

    return config


def _build_rich_text_content(report_file: Path) -> dict:
    """构建飞书富文本 (post) 消息体"""
    with open(report_file, 'r', encoding='utf-8') as f:
        full_report = f.read()

    lines = full_report.split('\n')

    # 提取标题
    title = "🔒 安全审计报告"
    for line in lines:
        if line.startswith("# "):
            title = line.lstrip("# ").strip()
            break

    # 构建富文本内容行
    content_lines = []

    # 摘要部分
    summary_items = []
    in_summary = False
    for line in lines:
        if '执行摘要' in line or '📊' in line:
            in_summary = True
            continue
        if in_summary:
            if line.startswith('---') or (line.startswith('##') and '执行摘要' not in line):
                break
            stripped = line.strip()
            if stripped.startswith('- '):
                summary_items.append(stripped)

    if summary_items:
        content_lines.append([{"tag": "text", "text": "📊 执行摘要", "style": ["bold"]}])
        for item in summary_items:
            content_lines.append([{"tag": "text", "text": item}])
        content_lines.append([{"tag": "text", "text": ""}])

    # 风险分布
    risk_items = []
    in_risk = False
    for line in lines:
        if '风险等级分布' in line:
            in_risk = True
            continue
        if in_risk:
            if line.startswith('---') or (line.startswith('#') and '风险' not in line):
                break
            stripped = line.strip()
            if stripped.startswith('- '):
                risk_items.append(stripped)

    if risk_items:
        content_lines.append([{"tag": "text", "text": "🎯 风险等级分布", "style": ["bold"]}])
        for item in risk_items:
            content_lines.append([{"tag": "text", "text": item}])
        content_lines.append([{"tag": "text", "text": ""}])

    # 优先修复清单
    fix_items = []
    in_fix = False
    for line in lines:
        if '优先修复清单' in line:
            in_fix = True
            continue
        if in_fix:
            stripped = line.strip()
            if stripped and not stripped.startswith('```'):
                fix_items.append(stripped)
            if len(fix_items) > 15:
                break

    if fix_items:
        content_lines.append([{"tag": "text", "text": "📋 优先修复清单", "style": ["bold"]}])
        for item in fix_items[:10]:
            content_lines.append([{"tag": "text", "text": item}])
        if len(fix_items) > 10:
            content_lines.append([
                {"tag": "text", "text": f"... 还有 {len(fix_items) - 10} 项，查看完整报告"}
            ])
    else:
        content_lines.append([{"tag": "text", "text": "✅ 未发现需要紧急修复的问题"}])

    return {
        "msg_type": "post",
        "content": {
            "post": {
                "zh_cn": {
                    "title": title,
                    "content": content_lines,
                }
            }
        }
    }


def _build_interactive_card(report_file: Path) -> dict:
    """构建飞书卡片消息（适用于 Bot API）"""
    with open(report_file, 'r', encoding='utf-8') as f:
        full_report = f.read()

    lines = full_report.split('\n')

    # 提取统计数据
    stats = {"pass": 0, "warning": 0, "fail": 0, "total": 0}
    for line in lines:
        if '通过:' in line:
            try:
                stats["pass"] = int(line.split(':')[-1].strip())
            except ValueError:
                pass
        elif '警告:' in line:
            try:
                stats["warning"] = int(line.split(':')[-1].strip())
            except ValueError:
                pass
        elif '失败:' in line:
            try:
                stats["fail"] = int(line.split(':')[-1].strip())
            except ValueError:
                pass
        elif '总计:' in line:
            try:
                stats["total"] = int(line.split(':')[-1].strip())
            except ValueError:
                pass

    # 确定卡片颜色
    if stats["fail"] > 0:
        header_color = "red"
        header_title = f"🔴 安全审计: 发现 {stats['fail']} 个问题"
    elif stats["warning"] > 0:
        header_color = "orange"
        header_title = f"🟡 安全审计: {stats['warning']} 个警告"
    else:
        header_color = "green"
        header_title = "🟢 安全审计: 全部通过"

    card = {
        "msg_type": "interactive",
        "card": {
            "header": {
                "title": {"tag": "plain_text", "content": header_title},
                "template": header_color,
            },
            "elements": [
                {
                    "tag": "div",
                    "text": {
                        "tag": "lark_md",
                        "content": (
                            f"**检查结果统计**\n"
                            f"✅ 通过: {stats['pass']}  |  "
                            f"⚠️ 警告: {stats['warning']}  |  "
                            f"❌ 失败: {stats['fail']}  |  "
                            f"📋 总计: {stats['total']}"
                        ),
                    },
                },
                {"tag": "hr"},
            ],
        },
    }

    # 添加修复清单
    fix_content = []
    in_fix = False
    for line in lines:
        if '优先修复清单' in line:
            in_fix = True
            continue
        if in_fix:
            stripped = line.strip()
            if stripped and not stripped.startswith('```'):
                fix_content.append(stripped)
            if len(fix_content) > 5:
                break

    if fix_content:
        card["card"]["elements"].append({
            "tag": "div",
            "text": {
                "tag": "lark_md",
                "content": "**📋 优先修复**\n" + "\n".join(fix_content[:5]),
            },
        })

    return card


def send_to_feishu_webhook(content: dict, webhook_url: str) -> bool:
    """通过 Webhook 发送消息（支持富文本和卡片）"""
    try:
        data = json.dumps(content, ensure_ascii=False).encode('utf-8')

        req = urllib.request.Request(
            webhook_url,
            data=data,
            headers={'Content-Type': 'application/json'},
        )

        with urllib.request.urlopen(req, timeout=10) as response:
            resp_data = json.loads(response.read().decode('utf-8'))
            if resp_data.get('code') == 0 or response.getcode() == 200:
                print_success("飞书消息发送成功")
                return True
            else:
                print_error(f"飞书返回错误: {resp_data}")
                return False

    except urllib.error.URLError as e:
        print_error(f"网络请求失败: {e}")
        return False
    except Exception as e:
        print_error(f"发送失败: {e}")
        return False


def send_via_openclaw_plugin(report_file: str, session_key: str = None) -> bool:
    """通过 OpenClaw 插件 API 发送报告"""
    config = load_feishu_config()
    if not config:
        return False

    # 查找飞书插件配置
    plugins = config.get('plugins', {}).get('installs', {})
    feishu_plugin = None
    for name, info in plugins.items():
        if 'feishu' in name.lower() or 'lark' in name.lower():
            feishu_plugin = info
            break

    if not feishu_plugin:
        print_info("未找到飞书插件配置，尝试使用 Webhook")
        return False

    # 尝试通过插件 API 端点发送
    api_base = feishu_plugin.get('apiEndpoint', '')
    if not api_base:
        print_info("飞书插件未配置 API 端点")
        return False

    report_path = Path(report_file)
    content = _build_rich_text_content(report_path)

    payload = {
        "action": "send_message",
        "session_key": session_key,
        "content": content,
    }

    try:
        data = json.dumps(payload, ensure_ascii=False).encode('utf-8')
        req = urllib.request.Request(
            api_base,
            data=data,
            headers={'Content-Type': 'application/json'},
        )

        with urllib.request.urlopen(req, timeout=15) as response:
            if response.getcode() == 200:
                print_success("通过 OpenClaw 插件 API 发送成功")
                return True
    except Exception as e:
        print_info(f"插件 API 发送失败 ({e})，回退到其他方式")

    return False


def format_report_for_feishu(report_file: Path, max_length: int = 2000) -> str:
    """格式化报告为纯文本（兼容旧版 webhook）"""
    with open(report_file, 'r', encoding='utf-8') as f:
        full_report = f.read()

    lines = full_report.split('\n')

    # 提取摘要
    summary_lines = []
    in_summary = False
    for line in lines:
        if '执行摘要' in line or '📊' in line:
            in_summary = True
        if in_summary:
            summary_lines.append(line)
            if len(summary_lines) > 15:
                break
        if in_summary and not line.strip() and len(summary_lines) > 5:
            break

    summary = '\n'.join(summary_lines[:15])

    # 提取问题
    issues = []
    in_failed = False
    for line in lines:
        if '优先修复清单' in line or '📋' in line:
            in_failed = True
            continue
        if in_failed:
            if line.startswith('##') or line.startswith('---'):
                break
            if line.strip().startswith('-') or line.strip().startswith('1.'):
                issues.append(line.strip())

    message = "🔒 安全审计报告\n\n"
    message += summary
    message += "\n\n---\n"

    if issues:
        message += "📋 需要关注的问题\n"
        for issue in issues[:5]:
            message += issue + "\n"
        if len(issues) > 5:
            message += f"\n... 还有 {len(issues) - 5} 个问题，查看完整报告\n"
    else:
        message += "✅ 未发现严重问题\n"

    if len(message) > max_length:
        message = message[:max_length - 30]
        message += "\n\n... (消息过长，查看完整报告)"

    return message


def send_report(report_file: str, webhook_url: str = None,
                session_key: str = None, format_type: str = "rich"):
    """发送报告的统一入口"""
    report_path = Path(report_file)
    if not report_path.exists():
        print_error(f"报告文件不存在: {report_file}")
        return False

    print_info(f"准备发送报告: {report_file}")

    # 1. 尝试 OpenClaw 插件 API
    if send_via_openclaw_plugin(report_file, session_key):
        return True

    # 2. 尝试 Webhook
    if not webhook_url:
        webhook_url = os.environ.get('FEISHU_WEBHOOK_URL', '')

    if webhook_url:
        if format_type == "card":
            content = _build_interactive_card(report_path)
        elif format_type == "rich":
            content = _build_rich_text_content(report_path)
        else:
            content = {
                "msg_type": "text",
                "content": {"text": format_report_for_feishu(report_path)},
            }
        return send_to_feishu_webhook(content, webhook_url)

    # 3. 兜底：保存格式化消息
    formatted = format_report_for_feishu(report_path)
    print_info("消息内容预览:")
    print("-" * 50)
    print(formatted[:500])
    print("-" * 50)
    print_success("报告已准备，请使用 OpenClaw message 工具发送或配置飞书 Webhook")

    return False


def main():
    import argparse

    parser = argparse.ArgumentParser(description="发送安全审计报告到飞书")
    parser.add_argument('report_file', help="报告文件路径")
    parser.add_argument('--webhook', help="飞书 Webhook URL")
    parser.add_argument('--session', help="飞书会话 Key")
    parser.add_argument(
        '--format', choices=['text', 'rich', 'card'], default='rich',
        help="消息格式: text(纯文本), rich(富文本), card(卡片) (默认: rich)",
    )

    args = parser.parse_args()

    if not Path(args.report_file).exists():
        print_error(f"报告文件不存在: {args.report_file}")
        sys.exit(1)

    success = send_report(
        args.report_file,
        webhook_url=args.webhook,
        session_key=args.session,
        format_type=args.format,
    )

    # 保存格式化消息副本
    output_file = Path(args.report_file).parent / f"{Path(args.report_file).stem}-feishu.txt"
    formatted = format_report_for_feishu(Path(args.report_file))
    with open(output_file, 'w', encoding='utf-8') as f:
        f.write(formatted)
    print_success(f"飞书消息副本已保存到: {output_file}")

    sys.exit(0 if success else 1)


if __name__ == "__main__":
    main()


Security scan verdicts — ClawScan: clean | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)