
Cursor 安全插件链:构建实时 SAST/DAST 辅助分析工作流
目录
- 摘要
- 一、开发安全左移核心理念与 Cursor 集成价值
- 1.1 什么是"安全左移"
- 1.2 SAST 与 DAST 基础概念
- 1.3 Cursor IDE 在安全工作流中的定位
- 1.4 集成价值与预期收益
- 二、基础环境搭建与必要安全插件安装
- 2.1 开发环境准备
- 2.1.1 操作系统要求
- 2.1.2 Cursor IDE 安装与基础配置
- 2.1.3 Node.js / Python 运行时环境
- 2.2 核心安全插件安装
- 2.2.1 Semgrep 插件安装与配置
- 2.2.2 SonarLint 插件集成
- 2.2.3 ESLint Security 插件
- 2.2.4 Bandit(Python 安全扫描)
- 2.2.5 OWASP ZAP 辅助工具
- 2.3 插件链依赖关系与版本兼容性
- 2.1 开发环境准备
- 三、配置自定义规则实现静态代码审计自动化
- 3.1 Semgrep 规则文件结构详解
- 3.2 编写第一条自定义 SAST 规则
- 3.2.1 规则语法基础
- 3.2.2 模式匹配与元变量
- 3.2.3 规则严重等级定义
- 3.3 多语言规则配置
- 3.3.1 JavaScript/TypeScript 规则
- 3.3.2 Python 规则
- 3.3.3 Java 规则
- 3.4 Cursor 中自动化触发配置
- 3.5 规则测试与验证
- 四、构建动态交互测试流程辅助运行时检测
- 4.1 DAST 基础原理与适用场景
- 4.2 OWASP ZAP 本地代理配置
- 4.3 Cursor 终端集成动态扫描
- 4.4 API 端点自动化探测
- 4.5 运行时漏洞报告解析
- 五、编写提示词引导 AI 识别常见漏洞模式
- 5.1 Cursor AI 安全审查提示词设计原则
- 5.2 SQL 注入识别提示词模板
- 5.3 XSS 漏洞检测提示词模板
- 5.4 认证与授权缺陷检测
- 5.5 敏感信息泄露检测
- 5.6 提示词链组合策略
- 六、实战演练:从代码编写到风险修复闭环
- 6.1 项目初始化与场景设定
- 6.2 编写含漏洞的示例代码
- 6.3 SAST 扫描触发与结果分析
- 6.4 AI 辅助漏洞解读
- 6.5 修复代码编写与验证
- 6.6 回归扫描确认修复有效
- 七、误报率优化策略与审计结果精准度提升
- 7.1 误报产生的常见原因
- 7.2 规则精细化调优
- 7.3 上下文感知过滤
- 7.4 白名单与抑制机制
- 7.5 持续反馈循环建立
- 八、团队协作规范与安全检查流程标准化
- 8.1 安全编码规范文档化
- 8.2 Git Hooks 集成安全扫描
- 8.3 Code Review 安全检查清单
- 8.4 安全事件响应流程
- 8.5 团队安全培训与知识共享
- 九、典型报错分析与插件链运行故障排查
- 9.1 插件安装失败排查
- 9.2 规则解析错误
- 9.3 扫描超时与性能问题
- 9.4 AI 提示词无响应处理
- 9.5 网络代理与证书问题
- 9.6 版本冲突解决
- 十、进阶技巧:扩展私有规则库与持续集成对接
- 10.1 企业私有规则库搭建
- 10.2 CI/CD 管道集成
- 10.3 安全报告自动生成
- 10.4 与漏洞管理平台对接
- 10.5 自定义仪表盘与度量
- 常见陷阱与问题排除
- 总结
- 详细参考资料
- 附录
- 附录 A:完整规则文件模板
- 附录 B:提示词速查表
- 附录 C:故障排查决策树
- 附录 D:推荐学习资源
摘要
在软件安全领域,“安全左移”(Shift-Left Security)已成为现代开发团队的核心实践理念。其核心思想是将安全检测从传统的上线前测试阶段,前移至代码编写阶段,从而在漏洞产生的第一时间发现并修复,大幅降低修复成本与安全风险。
Cursor 作为新一代 AI 驱动的代码编辑器,凭借其强大的 AI 辅助能力、丰富的插件生态以及高度可定制的扩展机制,为构建"编码即安全"的开发工作流提供了理想平台。本文将手把手带你从零开始,在 Cursor 中搭建一套完整的 SAST(静态应用安全测试)与 DAST(动态应用安全测试)辅助分析工作流。
本教程涵盖环境搭建、插件安装配置、自定义安全规则编写、AI 提示词工程、实战漏洞修复演练、误报优化、团队协作规范、故障排查以及 CI/CD 集成等十大主题。无论你是刚接触安全开发的新手,还是希望提升团队安全实践水平的技术负责人,都能从本文中获得可直接落地的操作指南。
适用读者:前端/后端开发工程师、安全工程师、DevSecOps 从业者、技术团队负责人
前置要求:基本的编程经验(熟悉至少一门编程语言)、了解基本的 Web 安全概念
预计阅读与实践时间:完整阅读约 2-3 小时,动手实践约 1-2 天
一、开发安全左移核心理念与 Cursor 集成价值
1.1 什么是"安全左移"
“安全左移”(Shift-Left Security)是软件工程中的一个重要理念,其名称来源于将软件开发生命周期(SDLC)画成一条从左到右的时间轴:
需求分析 → 设计 → 编码 → 测试 → 部署 → 运维
←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←←
传统安全检测位置(右侧)
→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→→
安全左移后的检测位置(左侧)
传统模式的问题:
在传统开发流程中,安全测试通常在开发完成后的 QA 阶段或上线前进行。这意味着:
- 漏洞发现时,相关代码可能已经与其他模块深度耦合
- 修复一个早期引入的漏洞,可能需要修改大量后续代码
- 修复成本随时间呈指数级增长(IBM 研究显示,生产环境修复成本是设计阶段的 100 倍)
- 安全团队与开发团队之间存在信息断层
安全左移的核心实践:
| 需求阶段 | 威胁建模 | STRIDE、DREAD |
| 设计阶段 | 安全架构评审 | 安全设计模式 |
| 编码阶段 | 实时代码审计 | SAST 工具(本文重点) |
| 测试阶段 | 动态安全测试 | DAST 工具(本文重点) |
| 部署阶段 | 容器/依赖扫描 | Trivy、Snyk |
| 运维阶段 | 运行时防护 | WAF、RASP |
1.2 SAST 与 DAST 基础概念
SAST(Static Application Security Testing,静态应用安全测试)
SAST 是一种在不运行程序的情况下,通过分析源代码、字节码或二进制文件来发现安全漏洞的技术。
工作原理:
源代码 → 词法分析 → 语法分析 → 构建 AST/数据流图 → 模式匹配/污点追踪 → 报告漏洞
核心能力:
- 模式匹配:检测已知危险函数调用(如 eval()、exec())
- 数据流分析:追踪用户输入从入口到危险函数的传播路径
- 控制流分析:检查条件分支是否覆盖所有安全场景
- 污点追踪:标记不可信数据,监控其是否未经净化就进入敏感操作
优势:
- 无需运行程序,编码阶段即可检测
- 覆盖率高,可扫描所有代码路径
- 能精确定位到具体代码行
局限:
- 存在误报(将安全代码标记为漏洞)
- 无法检测运行时才暴露的问题
- 对动态语言支持有限
DAST(Dynamic Application Security Testing,动态应用安全测试)
DAST 是在程序运行状态下,通过模拟攻击者行为来发现安全漏洞的技术。
工作原理:
运行中的应用 ← HTTP请求(含攻击载荷) ← DAST工具
运行中的应用 → HTTP响应 → DAST工具 → 分析响应 → 报告漏洞
核心能力:
- SQL 注入探测
- XSS(跨站脚本)检测
- CSRF(跨站请求伪造)验证
- 目录遍历测试
- 认证绕过尝试
- 安全头部检查
优势:
- 检测运行时真实漏洞,误报率低
- 不依赖源代码,可测试任何 Web 应用
- 能发现配置类问题(如缺少安全头)
局限:
- 需要应用处于运行状态
- 覆盖率取决于爬虫能力
- 无法定位到具体代码行
SAST 与 DAST 互补关系
┌─────────────────────────────────────────────────┐
│ 安全检测覆盖矩阵 │
├──────────────────┬──────────────┬───────────────┤
│ 漏洞类型 │ SAST │ DAST │
├──────────────────┼──────────────┼───────────────┤
│ SQL 注入 │ ✓(代码层) │ ✓(运行时) │
│ XSS │ ✓(代码层) │ ✓(运行时) │
│ 硬编码密钥 │ ✓ │ ✗ │
│ 配置错误 │ ✗ │ ✓ │
│ 业务逻辑漏洞 │ 部分 │ 部分 │
│ 依赖库漏洞 │ ✓(SCA) │ ✗ │
│ 认证绕过 │ 部分 │ ✓ │
└──────────────────┴──────────────┴───────────────┘
1.3 Cursor IDE 在安全工作流中的定位
Cursor 是基于 VS Code 深度定制的 AI 代码编辑器,它在安全工作流中具有独特优势:
AI 原生能力:
- 内置 AI 对话(Ctrl+L),可实时询问代码安全问题
- 代码补全时自动考虑安全最佳实践
- 可选中代码片段让 AI 进行安全审查
- 支持自定义系统提示词,定制安全审查角色
插件生态兼容:
- 完全兼容 VS Code 插件市场
- 支持 Semgrep、SonarLint 等主流安全插件
- 终端集成,可直接运行命令行安全工具
工作流集成:
- 支持 .cursorrules 项目级 AI 配置
- Git 集成,可在提交前触发安全检查
- 任务系统,可配置自动化安全扫描任务
1.4 集成价值与预期收益
将 SAST/DAST 工具链集成到 Cursor 开发环境中,可以带来以下收益:
对开发者:
- 编码时即时获得安全反馈,无需等待代码审查
- AI 辅助理解漏洞原理,提升安全编码能力
- 减少后期返工,提高开发效率
对团队:
- 统一安全标准,降低人为遗漏
- 缩短漏洞修复周期(从周级降至分钟级)
- 建立可量化的安全度量体系
对组织:
- 降低安全事件发生概率
- 减少安全修复成本
- 满足合规要求(如等保、GDPR、SOC2)
量化预期:
| 漏洞发现阶段 | 测试/上线后 | 编码时 | 左移 2-3 个阶段 |
| 平均修复时间 | 3-7 天 | 10-30 分钟 | 降低 95%+ |
| 安全相关返工率 | 15-25% | 3-5% | 降低 80%+ |
| 开发者安全意识 | 被动接受 | 主动防御 | 质变 |
二、基础环境搭建与必要安全插件安装
2.1 开发环境准备
2.1.1 操作系统要求
本教程支持以下操作系统:
| Windows | Windows 10 (1903+) | Windows 11 |
| macOS | macOS 11 (Big Sur) | macOS 14 (Sonoma) |
| Linux | Ubuntu 20.04 / Fedora 36 | Ubuntu 22.04+ |
硬件建议:
- 内存:至少 8GB(推荐 16GB,安全扫描工具较耗内存)
- 磁盘:至少 500MB 可用空间(插件+规则库)
- CPU:多核处理器有助于并行扫描
2.1.2 Cursor IDE 安装与基础配置
第一步:下载与安装
访问 Cursor 官方网站(cursor.com),根据操作系统下载对应版本:
# macOS (使用 Homebrew)
brew install –cask cursor
# Windows
# 下载安装包后双击运行,按向导完成安装
# Linux (Ubuntu/Debian)
wget https://download.cursor.com/linux/appimage/latest -O cursor.AppImage
chmod +x cursor.AppImage
./cursor.AppImage
第二步:初始配置
安装完成后,打开 Cursor 进行基础配置:
// settings.json – 通过 Ctrl+Shift+P → "Open Settings (JSON)" 打开
{
// 编辑器基础配置
"editor.fontSize": 14,
"editor.tabSize": 2,
"editor.formatOnSave": true,
// 终端配置(安全工具需要)
"terminal.integrated.defaultProfile.linux": "bash",
"terminal.integrated.defaultProfile.osx": "zsh",
"terminal.integrated.defaultProfile.windows": "PowerShell",
// 文件监视(SAST 实时扫描需要)
"files.watcherExclude": {
"**/node_modules/**": true,
"**/.git/**": true,
"**/dist/**": true,
"**/build/**": true
},
// 安全相关:禁止自动执行不受信任的工作区
"security.workspace.trust.enabled": true
}
第三步:安装必要的基础扩展
在 Cursor 中按 Ctrl+Shift+X 打开扩展市场,安装以下基础扩展:
- Python(如果开发 Python 项目)
- ESLint(如果开发 JavaScript/TypeScript 项目)
- Docker(如果需要容器化部署)
- GitLens(代码追溯,辅助安全审计)
2.1.3 Node.js / Python 运行时环境
安全插件链依赖运行时环境,请确保安装:
Node.js(v18+):
# 使用 nvm 安装(推荐)
# macOS/Linux
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install 18
nvm use 18
# 验证安装
node –version # 应显示 v18.x.x 或更高
npm –version # 应显示 9.x.x 或更高
# Windows
# 访问 https://nodejs.org 下载 LTS 版本安装
# 或使用 winget:
winget install OpenJS.NodeJS.LTS
Python(3.9+):
# macOS
brew install python@3.11
# Ubuntu/Debian
sudo apt update
sudo apt install python3.11 python3-pip python3-venv
# 验证安装
python3 –version # 应显示 3.11.x
pip3 –version
# Windows
winget install Python.Python.3.11
2.2 核心安全插件安装
2.2.1 Semgrep 插件安装与配置
Semgrep 是目前最流行的开源 SAST 工具之一,支持 30+ 编程语言,规则编写直观。
第一步:安装 Semgrep CLI
# 方式一:pip 安装(推荐)
pip3 install semgrep
# 方式二:macOS Homebrew
brew install semgrep
# 方式三:Docker(无需本地安装)
docker run –rm -v "${PWD}:/src" semgrep/semgrep semgrep –config=auto /src
# 验证安装
semgrep –version
# 应输出类似:semgrep version 1.67.0
第二步:安装 Cursor/VS Code Semgrep 扩展
第三步:配置 Semgrep 扩展
打开设置 JSON,添加 Semgrep 配置:
{
// Semgrep 扩展配置
"semgrep.path": "semgrep", // 如果不在 PATH 中,填写完整路径
"semgrep.scan.onType": true, // 输入时实时扫描
"semgrep.scan.onSave": true, // 保存时扫描
"semgrep.scan.configuration": [
"auto", // 自动选择规则
"./semgrep-rules/" // 本地自定义规则目录
],
"semgrep.severity.overrides": {
"INFO": "hint",
"WARNING": "warning",
"ERROR": "error"
},
// 排除不需要扫描的路径
"semgrep.exclude": [
"node_modules",
"dist",
"build",
".git",
"vendor",
"*.min.js",
"*.test.js"
]
}
第四步:验证 Semgrep 工作
创建一个测试文件 test-security.js:
// test-security.js – 用于验证 Semgrep 是否正常工作
// 这个文件故意包含一个安全问题
const express = require('express');
const app = express();
// 危险:直接使用用户输入构建 SQL 查询(SQL 注入漏洞)
app.get('/user', (req, res) => {
const userId = req.query.id;
// Semgrep 应该能检测到这里的 SQL 注入风险
const query = "SELECT * FROM users WHERE id = " + userId;
// 实际项目中这里会执行数据库查询
res.send(query);
});
app.listen(3000);
在 Cursor 中打开此文件,如果 Semgrep 配置正确,你应该能看到代码下方出现黄色/红色波浪线提示。
2.2.2 SonarLint 插件集成
SonarLint 是 SonarQube 的 IDE 插件版本,提供实时代码质量与安全检测。
安装步骤:
{
// SonarLint 配置
"sonarlint.connectedMode.project": {
// 如果有 SonarQube 服务器,配置连接
// 没有的话可以先使用独立模式
"connectionId": "",
"projectKey": ""
},
// 规则配置
"sonarlint.rules": {
// 启用所有安全热点规则
"javascript:S2076": { "level": "on" }, // OS 命令注入
"javascript:S2077": { "level": "on" }, // SQL 注入
"javascript:S2078": { "level": "on" }, // LDAP 注入
"javascript:S5131": { "level": "on" }, // XSS
"python:S2076": { "level": "on" },
"python:S2077": { "level": "on" }
},
// 分析触发
"sonarlint.analyzerProperties": {
"sonar.javascript.node.maxspace": "4096"
}
}
2.2.3 ESLint Security 插件
对于 JavaScript/TypeScript 项目,ESLint 安全插件是必备工具。
安装:
# 在项目目录中安装
npm install –save-dev eslint eslint-plugin-security eslint-plugin-no-unsanitized
配置 .eslintrc.json:
{
"env": {
"browser": true,
"node": true,
"es2022": true
},
"extends": [
"eslint:recommended",
"plugin:security/recommended-legacy",
"plugin:no-unsanitized/DOM"
],
"plugins": [
"security",
"no-unsanitized"
],
"rules": {
// 安全相关规则 – 设为 error 级别
"security/detect-eval-with-expression": "error",
"security/detect-child-process": "error",
"security/detect-disable-mustache-escape": "error",
"security/detect-no-csrf-before-method-override": "error",
"security/detect-non-literal-fs-filename": "warn",
"security/detect-non-literal-regexp": "warn",
"security/detect-non-literal-require": "error",
"security/detect-object-injection": "warn",
"security/detect-possible-timing-attacks": "error",
"security/detect-pseudoRandomBytes": "error",
"security/detect-unsafe-regex": "error",
"security/detect-buffer-noassert": "error",
// no-unsanitized 规则
"no-unsanitized/property": "error",
"no-unsanitized/method": "error"
}
}
2.2.4 Bandit(Python 安全扫描)
Bandit 是 Python 专用的 SAST 工具,能检测 Python 代码中的常见安全问题。
安装:
# pip 安装
pip3 install bandit
# 验证
bandit –version
Cursor 集成:
创建 .bandit 配置文件(项目根目录):
# .bandit – Bandit 配置文件
[bandit]
# 排除的目录
exclude: /tests,/venv,/.git,/node_modules,/migrations
# 扫描目标
targets: ./src,./app
# 严重等级过滤(low, medium, high)
level: low
# 置信度过滤
confidence: low
# 输出格式
format: json
# 输出文件
output: ./reports/bandit-report.json
在 Cursor 终端中运行:
# 扫描整个项目
bandit -r ./src -f json -o ./reports/bandit-report.json
# 扫描单个文件
bandit ./src/app.py
# 只显示高严重性问题
bandit -r ./src -ll
# 使用自定义配置
bandit -r ./src -c .bandit
2.2.5 OWASP ZAP 辅助工具
OWASP ZAP(Zed Attack Proxy)是用于 DAST 的开源工具。
安装:
# macOS
brew install –cask owasp-zap
# Linux (Ubuntu)
sudo apt install zaproxy
# 或下载最新版
wget https://github.com/zaproxy/zaproxy/releases/download/v2.15.0/ZAP_2.15.0_Linux.tar.gz
tar -xzf ZAP_2.15.0_Linux.tar.gz
# Windows
# 从 https://www.zaproxy.org/download/ 下载安装包
# Docker 方式(推荐,无需本地安装)
docker pull ghcr.io/zaproxy/zaproxy:stable
Cursor 终端集成脚本(创建 scripts/dast-scan.sh):
#!/bin/bash
# dast-scan.sh – 在 Cursor 终端中运行 DAST 扫描
# 用法: ./scripts/dast-scan.sh <目标URL>
TARGET_URL=${1:-"http://localhost:3000"}
REPORT_DIR="./reports/dast"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
# 创建报告目录
mkdir -p ${REPORT_DIR}
echo "=========================================="
echo " OWASP ZAP DAST 扫描"
echo " 目标: ${TARGET_URL}"
echo " 时间: $(date)"
echo "=========================================="
# 使用 Docker 运行 ZAP 基线扫描
docker run –rm \\
–network="host" \\
-v $(pwd)/${REPORT_DIR}:/zap/reports \\
ghcr.io/zaproxy/zaproxy:stable \\
zap-baseline.py \\
-t ${TARGET_URL} \\
-r /zap/reports/zap-report-${TIMESTAMP}.html \\
-J /zap/reports/zap-report-${TIMESTAMP}.json \\
-c zap-rules.conf \\
-T 10 \\
-a
echo ""
echo "扫描完成!报告已保存至: ${REPORT_DIR}/"
echo "HTML 报告: ${REPORT_DIR}/zap-report-${TIMESTAMP}.html"
echo "JSON 报告: ${REPORT_DIR}/zap-report-${TIMESTAMP}.json"
2.3 插件链依赖关系与版本兼容性
以下是完整的插件链依赖关系图:
┌─────────────────────────────────────────────────────────┐
│ Cursor IDE │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Semgrep │ │ SonarLint │ │ ESLint │ │
│ │ Extension │ │ Extension │ │ Security │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Semgrep │ │ SonarQube │ │ Node.js │ │
│ │ CLI (Py) │ │ Analyzer │ │ Runtime │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │ │
├─────────┼─────────────────┼─────────────────┼───────────┤
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Python 3.9+ Runtime │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Bandit │ │ OWASP ZAP │ ← 独立运行 │
│ │ (Python) │ │ (Java/Docker)│ │
│ └─────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
版本兼容性矩阵(截至 2026 年):
| Cursor | 最新稳定版 | 0.40+ | 保持更新 |
| Semgrep CLI | 1.70+ | 1.50+ | pip 安装 |
| Semgrep Extension | 最新 | 1.12+ | 与 CLI 版本匹配 |
| SonarLint | 最新 | 4.0+ | 独立模式可用 |
| Node.js | 20 LTS | 18 LTS | ESLint 需要 |
| Python | 3.11+ | 3.9+ | Semgrep/Bandit 需要 |
| ESLint | 9.x | 8.x | 配合安全插件 |
| Bandit | 1.7+ | 1.7.0 | Python 项目 |
| OWASP ZAP | 2.15+ | 2.12+ | Docker 推荐 |
三、配置自定义规则实现静态代码审计自动化
3.1 Semgrep 规则文件结构详解
Semgrep 规则使用 YAML 格式编写,存放在项目中的 semgrep-rules/ 目录下。一个完整的规则文件结构如下:
# semgrep-rules/custom-security.yaml
# 自定义安全规则文件
rules:
# 每条规则是一个列表项
– id: rule–unique–identifier # 规则唯一标识符(必填)
patterns: # 匹配模式(核心)
– pattern: |
dangerous_function(…)
message: | # 检测到问题时的提示信息(必填)
检测到潜在的安全问题:使用了危险函数。
请使用安全的替代方案。
languages: # 适用语言(必填)
– javascript
– typescript
severity: ERROR # 严重等级:INFO/WARNING/ERROR(必填)
metadata: # 元数据(可选但推荐)
category: security # 分类
cwe: "CWE-89" # CWE 编号
owasp: "A03:2021 Injection" # OWASP 分类
confidence: HIGH # 置信度
likelihood: HIGH # 可能性
impact: HIGH # 影响
subcategory:
– vuln # vuln / audit / guardrail
references: # 参考链接
– https://owasp.org/Top10/A03_2021–Injection/
technology:
– express # 相关技术栈
fix: | # 自动修复建议(可选)
safe_function(…)
规则文件目录结构建议:
project-root/
├── semgrep-rules/
│ ├── injection/ # 注入类漏洞
│ │ ├── sql-injection.yaml
│ │ ├── command-injection.yaml
│ │ └── ldap-injection.yaml
│ ├── xss/ # XSS 类漏洞
│ │ ├── reflected-xss.yaml
│ │ └── stored-xss.yaml
│ ├── auth/ # 认证授权类
│ │ ├── weak-crypto.yaml
│ │ ├── hardcoded-secrets.yaml
│ │ └── insecure-auth.yaml
│ ├── data-exposure/ # 数据泄露类
│ │ ├── sensitive-logging.yaml
│ │ └── insecure-storage.yaml
│ ├── config/ # 配置类
│ │ ├── cors-misconfiguration.yaml
│ │ └── missing-security-headers.yaml
│ └── custom/ # 业务自定义规则
│ ├── api-security.yaml
│ └── business-logic.yaml
├── .semgrepignore # 忽略文件
└── semgrep.yaml # 全局配置
3.2 编写第一条自定义 SAST 规则
3.2.1 规则语法基础
Semgrep 的匹配语法非常直观,它使用目标语言的实际代码语法来编写模式:
# semgrep-rules/first-rule.yaml
# 第一条自定义规则:检测 console.log 中的敏感信息
rules:
– id: no–sensitive–data–in–console
patterns:
# 匹配 console.log() 中包含特定变量的调用
– pattern: |
console.log(…, $SENSITIVE, …)
# 限定变量名包含敏感关键词
– metavariable-regex:
metavariable: $SENSITIVE
regex: .*(password|secret|token|apikey|api_key).*
message: >
检测到 console.log 中可能输出敏感信息 '$SENSITIVE'。
生产环境中不应将密码、密钥等敏感数据输出到控制台。
请使用日志脱敏处理或移除该输出。
languages:
– javascript
– typescript
severity: WARNING
metadata:
category: security
cwe: "CWE-532"
confidence: MEDIUM
3.2.2 模式匹配与元变量
Semgrep 支持多种匹配模式:
rules:
# 1. 精确匹配 – 匹配特定函数调用
– id: detect–eval–usage
pattern: eval($X)
message: "禁止使用 eval(),存在代码注入风险"
languages: [javascript]
severity: ERROR
# 2. 元变量匹配 – $X 匹配任意表达式
– id: detect–sql–concat
patterns:
– pattern: |
$QUERY = "…" + $INPUT
– metavariable-regex:
metavariable: $QUERY
regex: .*(sql|query|stmt).*
message: "检测到 SQL 查询字符串拼接,可能存在 SQL 注入"
languages: [javascript, python, java]
severity: ERROR
# 3. 模式内省略号 – … 匹配任意数量参数
– id: detect–exec–with–user–input
pattern: |
child_process.exec(…, $INPUT, …)
message: "exec() 接收外部输入可能导致命令注入"
languages: [javascript]
severity: ERROR
# 4. 否定模式 – 排除特定情况
– id: detect–hardcoded–secret–not–env
patterns:
– pattern: |
const $VAR = "$VALUE"
– metavariable-regex:
metavariable: $VAR
regex: .*(password|secret|key|token).*
– pattern-not: |
const $VAR = process.env.$ENV_VAR
– pattern-not: |
const $VAR = ""
message: "检测到硬编码的敏感信息,请使用环境变量"
languages: [javascript, typescript]
severity: ERROR
# 5. 数据流追踪(taint mode)
– id: taint–sql–injection
mode: taint
pattern-sources:
– pattern: req.query.$PARAM
– pattern: req.body.$PARAM
– pattern: req.params.$PARAM
pattern-sinks:
– pattern: db.query($SQL, …)
– pattern: connection.execute($SQL, …)
pattern-sanitizers:
– pattern: sanitize($X)
– pattern: escape($X)
message: "用户输入未经净化直接进入 SQL 查询"
languages: [javascript]
severity: ERROR
3.2.3 规则严重等级定义
# 严重等级使用指南
severity: INFO # 信息提示:最佳实践建议,不阻塞开发
severity: WARNING # 警告:潜在风险,建议修复
severity: ERROR # 错误:高危漏洞,必须修复
# 在 Cursor 中的显示效果:
# INFO → 灰色提示(不干扰编码)
# WARNING → 黄色波浪线(引起注意)
# ERROR → 红色波浪线(必须处理)
3.3 多语言规则配置
3.3.1 JavaScript/TypeScript 规则
# semgrep-rules/injection/sql-injection-js.yaml
rules:
# 检测 Express 中的 SQL 注入
– id: express–sql–injection–string–concat
patterns:
– pattern-either:
# 模式1:字符串拼接
– pattern: |
$DB.query("…" + $INPUT + "…", …)
# 模式2:模板字符串(无参数化)
– pattern: |
$DB.query(`…${$INPUT}…`, …)
# 模式3:字符串拼接后传入
– pattern: |
const $SQL = "…" + $INPUT + "…";
…
$DB.query($SQL, …)
– pattern-not: |
$DB.query("…", [$PARAM], …)
message: >
检测到 SQL 查询使用字符串拼接构造。
请使用参数化查询(Prepared Statement)防止 SQL 注入。
修复示例:
✗ db.query("SELECT * FROM users WHERE id = " + userId)
✓ db.query("SELECT * FROM users WHERE id = ?", [userId])
languages: [javascript, typescript]
severity: ERROR
metadata:
cwe: "CWE-89"
owasp: "A03:2021"
category: security
confidence: HIGH
fix: |
$DB.query("…", [$INPUT], …)
# 检测命令注入
– id: js–command–injection
patterns:
– pattern-either:
– pattern: |
require('child_process').exec($CMD)
– pattern: |
require('child_process').execSync($CMD)
– pattern: |
import { exec } from 'child_process';
…
exec($CMD)
– pattern-not: |
exec("ls") # 排除硬编码的安全命令
message: >
检测到使用 child_process.exec() 执行命令。
如果命令包含用户输入,将导致命令注入漏洞。
请使用 execFile() 或 spawn() 并传入参数数组。
languages: [javascript, typescript]
severity: ERROR
metadata:
cwe: "CWE-78"
confidence: MEDIUM
# 检测 XSS – innerHTML 赋值
– id: js–xss–innerhtml
patterns:
– pattern: |
$EL.innerHTML = $VALUE
– pattern-not: |
$EL.innerHTML = ""
– pattern-not: |
$EL.innerHTML = "…" # 纯字符串字面量
message: >
检测到直接设置 innerHTML,如果 $VALUE 包含用户输入,
将导致存储型/反射型 XSS 漏洞。
请使用 textContent 或 DOMPurify.sanitize() 处理。
languages: [javascript, typescript]
severity: ERROR
metadata:
cwe: "CWE-79"
3.3.2 Python 规则
# semgrep-rules/injection/sql-injection-python.yaml
rules:
# Python SQL 注入检测
– id: python–sql–injection–format
patterns:
– pattern-either:
# f-string 格式化
– pattern: |
$CURSOR.execute(f"…{$VAR}…")
# % 格式化
– pattern: |
$CURSOR.execute("…%s…" % $VAR)
# .format()
– pattern: |
$CURSOR.execute("…{}…".format($VAR))
# 字符串拼接
– pattern: |
$CURSOR.execute("…" + $VAR + "…")
– pattern-not: |
$CURSOR.execute("…", ($VAR,))
– pattern-not: |
$CURSOR.execute("…", [$VAR])
message: >
检测到 SQL 查询使用字符串格式化构造。
请使用参数化查询:cursor.execute("SELECT … WHERE id = %s", (user_id,))
languages: [python]
severity: ERROR
metadata:
cwe: "CWE-89"
confidence: HIGH
# Python 命令注入
– id: python–command–injection
patterns:
– pattern-either:
– pattern: os.system($CMD)
– pattern: os.popen($CMD)
– pattern: subprocess.call($CMD, shell=True, …)
– pattern: subprocess.run($CMD, shell=True, …)
– pattern: subprocess.Popen($CMD, shell=True, …)
message: >
检测到可能的命令注入风险。
os.system() 和 shell=True 会使命令经过 shell 解释,
如果包含用户输入则存在命令注入风险。
请使用 subprocess.run([…], shell=False) 并传入参数列表。
languages: [python]
severity: ERROR
metadata:
cwe: "CWE-78"
# Python 不安全的反序列化
– id: python–insecure–deserialization
patterns:
– pattern-either:
– pattern: pickle.loads($DATA)
– pattern: pickle.load($FILE)
– pattern: yaml.load($DATA)
– pattern-not: |
yaml.load($DATA, Loader=yaml.SafeLoader)
– pattern-not: |
yaml.safe_load($DATA)
message: >
检测到不安全的反序列化操作。
pickle 和 yaml.load() 可被利用执行任意代码。
请使用 yaml.safe_load() 或添加数据验证。
languages: [python]
severity: ERROR
metadata:
cwe: "CWE-502"
# Python 硬编码密钥
– id: python–hardcoded–secret
patterns:
– pattern: |
$VAR = "$VALUE"
– metavariable-regex:
metavariable: $VAR
regex: (?i).*(password|passwd|secret|api_key|apikey|token|private_key).*
– metavariable-regex:
metavariable: $VALUE
regex: .{8,} # 至少8个字符,排除空字符串
– pattern-not: |
$VAR = os.environ.get(…)
– pattern-not: |
$VAR = os.getenv(…)
message: >
检测到硬编码的敏感信息。
请使用环境变量或密钥管理服务存储敏感信息。
languages: [python]
severity: ERROR
metadata:
cwe: "CWE-798"
3.3.3 Java 规则
# semgrep-rules/injection/sql-injection-java.yaml
rules:
# Java SQL 注入
– id: java–sql–injection
patterns:
– pattern-either:
– pattern: |
$STMT.executeQuery("…" + $INPUT + "…")
– pattern: |
$STMT.executeUpdate("…" + $INPUT + "…")
– pattern: |
$CONN.createStatement().executeQuery("…" + $INPUT + "…")
– pattern-not: |
$CONN.prepareStatement("…")
message: >
检测到 SQL 语句拼接。请使用 PreparedStatement 参数化查询。
✗ stmt.executeQuery("SELECT * FROM users WHERE id = " + userId)
✓ PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setString(1, userId);
languages: [java]
severity: ERROR
metadata:
cwe: "CWE-89"
# Java 不安全的随机数
– id: java–insecure–random
patterns:
– pattern: new Random(…)
– pattern-not-inside: |
// 测试代码中允许
@Test
…
message: >
检测到使用 java.util.Random,该类不是密码学安全的。
安全场景请使用 java.security.SecureRandom。
languages: [java]
severity: WARNING
metadata:
cwe: "CWE-330"
3.4 Cursor 中自动化触发配置
在 Cursor 中配置保存时自动触发安全扫描:
方式一:通过 Semgrep 扩展设置(已在 2.2.1 配置)
方式二:通过 VS Code Tasks 配置
创建 .vscode/tasks.json:
{
"version": "2.0.0",
"tasks": [
{
"label": "Semgrep: 扫描当前文件",
"type": "shell",
"command": "semgrep",
"args": [
"–config", "auto",
"–config", "./semgrep-rules/",
"–error",
"${file}"
],
"group": "test",
"presentation": {
"reveal": "silent",
"panel": "dedicated",
"clear": true
},
"problemMatcher": {
"owner": "semgrep",
"fileLocation": "relative",
"pattern": {
"regexp": "^(.+):(\\\\d+):(\\\\d+):\\\\s+(.+)$",
"file": 1,
"line": 2,
"column": 3,
"message": 4
}
}
},
{
"label": "Semgrep: 全项目扫描",
"type": "shell",
"command": "semgrep",
"args": [
"–config", "auto",
"–config", "./semgrep-rules/",
"–json",
"–output", "./reports/semgrep-report.json",
"."
],
"group": "test",
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{
"label": "Bandit: Python 安全扫描",
"type": "shell",
"command": "bandit",
"args": [
"-r", "./src",
"-f", "custom",
"–msg-template", "{abspath}:{line}: [{severity}] {msg} ({test_id})"
],
"group": "test",
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
},
{
"label": "ESLint: 安全规则检查",
"type": "shell",
"command": "npx",
"args": [
"eslint",
"–ext", ".js,.ts,.jsx,.tsx",
"–rule", "security/detect-eval-with-expression: error",
"."
],
"group": "test"
},
{
"label": "安全扫描: 全部",
"dependsOn": [
"Semgrep: 全项目扫描",
"ESLint: 安全规则检查"
],
"group": {
"kind": "test",
"isDefault": true
}
}
]
}
方式三:配置保存时自动运行(通过扩展设置)
// settings.json 追加
{
// 保存时运行任务
"tasks.autoRun": true,
// 文件保存时触发
"[javascript]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
}
3.5 规则测试与验证
编写规则后,必须测试其正确性:
# 测试单条规则
semgrep –config semgrep-rules/injection/sql-injection-js.yaml –test
# 使用测试用例验证
# 创建测试文件:semgrep-rules/injection/test-sql-injection.js
# 测试文件内容示例(包含注释标记):
// semgrep-rules/injection/test-sql-injection.js
// 测试用例文件 – 用于验证规则准确性
const express = require('express');
const mysql = require('mysql');
const db = mysql.createConnection({});
// ruleid: express-sql-injection-string-concat
// ↑ 这行注释告诉 Semgrep 下一行应该被匹配
app.get('/user', (req, res) => {
const userId = req.query.id;
db.query("SELECT * FROM users WHERE id = " + userId);
});
// ruleid: express-sql-injection-string-concat
app.get('/search', (req, res) => {
const keyword = req.query.q;
db.query(`SELECT * FROM products WHERE name LIKE '%${keyword}%'`);
});
// ok: express-sql-injection-string-concat
// ↑ 这行注释告诉 Semgrep 下一行不应该被匹配
app.get('/safe-user', (req, res) => {
const userId = req.query.id;
db.query("SELECT * FROM users WHERE id = ?", [userId]);
});
// ok: express-sql-injection-string-concat
app.get('/safe-search', (req, res) => {
const keyword = req.query.q;
db.query("SELECT * FROM products WHERE name LIKE ?", [`%${keyword}%`]);
});
# 运行测试
semgrep –config semgrep-rules/injection/sql-injection-js.yaml \\
–test semgrep-rules/injection/
# 期望输出:
# ✓ express-sql-injection-string-concat
# 2/2 findings matched (2 true positives, 0 false positives)
# All tests passed!
四、构建动态交互测试流程辅助运行时检测
4.1 DAST 基础原理与适用场景
DAST(动态应用安全测试)通过向运行中的应用发送恶意请求并分析响应来发现漏洞。
适用场景:
- Web API 安全测试
- 表单输入验证测试
- 认证/授权流程测试
- 安全配置检查(CORS、安全头等)
- 会话管理测试
不适用场景:
- 代码逻辑审查(应使用 SAST)
- 第三方依赖漏洞(应使用 SCA)
- 加密算法正确性验证
4.2 OWASP ZAP 本地代理配置
配置 Cursor 终端代理:
# 启动 ZAP 作为本地代理
# 方式一:直接启动
zap.sh -daemon -port 8080 -config api.disablekey=true
# 方式二:Docker 启动(推荐)
docker run -d –name zap-proxy \\
-p 8080:8080 \\
-p 8090:8090 \\
ghcr.io/zaproxy/zaproxy:stable \\
zap.sh -daemon -port 8080 -config api.disablekey=true
创建 ZAP 配置文件 zap-config.conf:
# zap-config.conf – ZAP 扫描配置
# 扫描策略
scanner.strength=HIGH
scanner.threshold=MEDIUM
# 排除路径(不扫描静态资源)
globalexcludeurl=.*\\\\.css
globalexcludeurl=.*\\\\.js
globalexcludeurl=.*\\\\.png
globalexcludeurl=.*\\\\.jpg
globalexcludeurl=.*\\\\.gif
globalexcludeurl=.*\\\\.ico
# 认证配置(如果应用需要登录)
# authentication.method=form
# authentication.loginurl=http://localhost:3000/login
# authentication.username=admin
# authentication.password=admin123
# 扫描超时设置
scanner.timeout=300
spider.maxduration=10
4.3 Cursor 终端集成动态扫描
创建 scripts/dast-workflow.sh:
#!/bin/bash
# dast-workflow.sh – 完整的 DAST 工作流脚本
# 在 Cursor 终端中运行: ./scripts/dast-workflow.sh
set -e # 遇到错误立即退出
# ==================== 配置区 ====================
TARGET_URL="${1:-http://localhost:3000}"
ZAP_PORT=8080
REPORT_DIR="./reports/dast"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
SCAN_TIMEOUT=300 # 扫描超时(秒)
# ================================================
echo "╔══════════════════════════════════════════╗"
echo "║ DAST 动态安全扫描工作流 ║"
echo "╠══════════════════════════════════════════╣"
echo "║ 目标: ${TARGET_URL}"
echo "║ 时间: $(date '+%Y-%m-%d %H:%M:%S')"
echo "║ 报告: ${REPORT_DIR}/"
echo "╚══════════════════════════════════════════╝"
echo ""
# 步骤1:创建报告目录
mkdir -p ${REPORT_DIR}
# 步骤2:检查目标是否可达
echo "[1/6] 检查目标可达性…"
if ! curl -s -o /dev/null -w "%{http_code}" ${TARGET_URL} | grep -q "200\\|301\\|302"; then
echo "❌ 错误:目标 ${TARGET_URL} 不可达!"
echo " 请确保应用已启动。"
exit 1
fi
echo "✅ 目标可达"
# 步骤3:启动 ZAP(如果未运行)
echo "[2/6] 检查 ZAP 代理状态…"
if ! curl -s http://localhost:${ZAP_PORT}/JSON/core/view/version/ > /dev/null 2>&1; then
echo " 启动 ZAP 代理…"
docker run -d –name zap-scan \\
–network="host" \\
ghcr.io/zaproxy/zaproxy:stable \\
zap.sh -daemon -port ${ZAP_PORT} -config api.disablekey=true
# 等待 ZAP 启动
echo " 等待 ZAP 启动…"
sleep 15
fi
echo "✅ ZAP 代理就绪"
# 步骤4:执行爬虫(Spider)
echo "[3/6] 执行网站爬虫…"
SPIDER_ID=$(curl -s "http://localhost:${ZAP_PORT}/JSON/spider/action/scan/?url=${TARGET_URL}" | \\
python3 -c "import sys,json; print(json.load(sys.stdin)['scan'])")
# 等待爬虫完成
while true; do
PROGRESS=$(curl -s "http://localhost:${ZAP_PORT}/JSON/spider/view/status/?scanId=${SPIDER_ID}" | \\
python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
echo " 爬虫进度: ${PROGRESS}%"
if [ "$PROGRESS" = "100" ]; then
break
fi
sleep 5
done
echo "✅ 爬虫完成"
# 步骤5:执行主动扫描
echo "[4/6] 执行主动安全扫描(这可能需要几分钟)…"
SCAN_ID=$(curl -s "http://localhost:${ZAP_PORT}/JSON/ascan/action/scan/?url=${TARGET_URL}" | \\
python3 -c "import sys,json; print(json.load(sys.stdin)['scan'])")
# 等待扫描完成
ELAPSED=0
while true; do
PROGRESS=$(curl -s "http://localhost:${ZAP_PORT}/JSON/ascan/view/status/?scanId=${SCAN_ID}" | \\
python3 -c "import sys,json; print(json.load(sys.stdin)['status'])")
echo " 扫描进度: ${PROGRESS}%"
if [ "$PROGRESS" = "100" ]; then
break
fi
sleep 10
ELAPSED=$((ELAPSED + 10))
if [ $ELAPSED -ge $SCAN_TIMEOUT ]; then
echo "⚠️ 扫描超时,使用当前结果"
break
fi
done
echo "✅ 主动扫描完成"
# 步骤6:生成报告
echo "[5/6] 生成安全报告…"
# HTML 报告
curl -s "http://localhost:${ZAP_PORT}/OTHER/core/other/htmlreport/" > \\
"${REPORT_DIR}/zap-report-${TIMESTAMP}.html"
# JSON 报告
curl -s "http://localhost:${ZAP_PORT}/JSON/core/view/alerts/?baseurl=${TARGET_URL}" > \\
"${REPORT_DIR}/zap-alerts-${TIMESTAMP}.json"
# 解析并输出摘要
echo "[6/6] 扫描结果摘要:"
echo "─────────────────────────────────────────"
python3 << 'EOF'
import json, sys
try:
with open(f"${REPORT_DIR}/zap-alerts-${TIMESTAMP}.json") as f:
data = json.load(f)
alerts = data.get('alerts', [])
# 按风险等级统计
risk_count = {'High': 0, 'Medium': 0, 'Low': 0, 'Informational': 0}
for alert in alerts:
risk = alert.get('risk', 'Informational')
risk_count[risk] = risk_count.get(risk, 0) + 1
print(f" 🔴 高危: {risk_count['High']}")
print(f" 🟠 中危: {risk_count['Medium']}")
print(f" 🟡 低危: {risk_count['Low']}")
print(f" 🔵 信息: {risk_count['Informational']}")
print(f" ─────────────────")
print(f" 总计: {len(alerts)} 个发现")
# 输出高危详情
if risk_count['High'] > 0:
print(f"\\n ⚠️ 高危漏洞详情:")
for alert in alerts:
if alert.get('risk') == 'High':
print(f" – {alert.get('name')}")
print(f" URL: {alert.get('url')}")
print(f" 描述: {alert.get('description', '')[:100]}…")
print()
except Exception as e:
print(f" 解析报告时出错: {e}")
EOF
echo ""
echo "══════════════════════════════════════════"
echo "扫描完成!"
echo "HTML 报告: ${REPORT_DIR}/zap-report-${TIMESTAMP}.html"
echo "JSON 数据: ${REPORT_DIR}/zap-alerts-${TIMESTAMP}.json"
echo "══════════════════════════════════════════"
4.4 API 端点自动化探测
对于 REST API 项目,可以使用专门的 API 扫描脚本:
#!/usr/bin/env python3
"""
api_security_scanner.py – API 端点安全扫描器
在 Cursor 终端中运行: python3 scripts/api_security_scanner.py
功能:
– 自动发现 API 端点
– 测试常见注入点
– 检查安全头部
– 验证认证机制
"""
import requests
import json
import time
from datetime import datetime
from urllib.parse import urljoin, urlparse
from typing import List, Dict, Optional
class APISecurityScanner:
"""API 安全扫描器"""
def __init__(self, base_url: str, auth_token: Optional[str] = None):
self.base_url = base_url.rstrip('/')
self.auth_token = auth_token
self.findings: List[Dict] = []
self.session = requests.Session()
if auth_token:
self.session.headers['Authorization'] = f'Bearer {auth_token}'
def check_security_headers(self, url: str) –> None:
"""检查安全响应头"""
try:
response = self.session.get(url, timeout=10)
headers = response.headers
# 应该存在的安全头
required_headers = {
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': None, # DENY 或 SAMEORIGIN
'X-XSS-Protection': None, # 1; mode=block
'Strict-Transport-Security': None, # max-age=…
'Content-Security-Policy': None,
}
# 不应该存在的头
forbidden_headers = ['Server', 'X-Powered-By']
for header, expected in required_headers.items():
if header not in headers:
self._add_finding(
severity='MEDIUM',
title=f'缺少安全头: {header}',
url=url,
description=f'响应中缺少 {header} 头',
recommendation=f'添加 {header} 响应头'
)
for header in forbidden_headers:
if header in headers:
self._add_finding(
severity='LOW',
title=f'信息泄露头: {header}',
url=url,
description=f'响应包含 {header}: {headers[header]}',
recommendation=f'移除 {header} 响应头'
)
except requests.RequestException as e:
print(f" ⚠️ 请求失败: {url} – {e}")
def test_sql_injection(self, url: str, params: Dict) –> None:
"""测试 SQL 注入"""
sqli_payloads = [
"' OR '1'='1",
"1' OR '1'='1'–",
"1; DROP TABLE users–",
"' UNION SELECT NULL–",
]
for param, value in params.items():
for payload in sqli_payloads:
test_params = params.copy()
test_params[param] = payload
try:
response = self.session.get(url, params=test_params, timeout=10)
# 检查响应中是否有 SQL 错误信息
sql_errors = ['sql', 'syntax', 'mysql', 'postgresql', 'oracle',
'sqlite', 'error', 'exception']
response_lower = response.text.lower()
if any(err in response_lower for err in sql_errors):
self._add_finding(
severity='HIGH',
title='潜在 SQL 注入',
url=url,
description=f'参数 {param} 可能存在 SQL 注入',
evidence=f'Payload: {payload}',
recommendation='使用参数化查询'
)
except requests.RequestException:
pass
def test_xss(self, url: str, params: Dict) –> None:
"""测试 XSS"""
xss_payloads = [
'<script>alert(1)</script>',
'"><img src=x onerror=alert(1)>',
"javascript:alert(1)",
]
for param, value in params.items():
for payload in xss_payloads:
test_params = params.copy()
test_params[param] = payload
try:
response = self.session.get(url, params=test_params, timeout=10)
# 检查 payload 是否被原样反射
if payload in response.text:
self._add_finding(
severity='HIGH',
title='反射型 XSS',
url=url,
description=f'参数 {param} 存在 XSS 漏洞',
evidence=f'Payload 被原样反射: {payload}',
recommendation='对所有输出进行 HTML 编码'
)
except requests.RequestException:
pass
def _add_finding(self, severity: str, title: str, url: str,
description: str, evidence: str = '',
recommendation: str = '') –> None:
"""添加发现"""
finding = {
'severity': severity,
'title': title,
'url': url,
'description': description,
'evidence': evidence,
'recommendation': recommendation,
'timestamp': datetime.now().isoformat()
}
self.findings.append(finding)
# 实时输出
icon = {'HIGH': '🔴', 'MEDIUM': '🟠', 'LOW': '🟡', 'INFO': '🔵'}
print(f" {icon.get(severity, '⚪')} [{severity}] {title}")
print(f" URL: {url}")
def generate_report(self, output_path: str) –> None:
"""生成 JSON 报告"""
report = {
'scan_info': {
'target': self.base_url,
'timestamp': datetime.now().isoformat(),
'total_findings': len(self.findings)
},
'findings': self.findings
}
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2)
print(f"\\n📄 报告已保存: {output_path}")
# ==================== 使用示例 ====================
if __name__ == '__main__':
# 配置扫描目标
TARGET = 'http://localhost:3000'
print("╔══════════════════════════════════════╗")
print("║ API 安全扫描器 v1.0 ║")
print("╚══════════════════════════════════════╝")
print(f"\\n目标: {TARGET}")
print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\\n")
scanner = APISecurityScanner(TARGET)
# 1. 检查安全头
print("[1/3] 检查安全响应头…")
scanner.check_security_headers(TARGET)
# 2. 测试 SQL 注入
print("\\n[2/3] 测试 SQL 注入…")
scanner.test_sql_injection(
f"{TARGET}/api/users",
{'id': '1', 'name': 'test'}
)
# 3. 测试 XSS
print("\\n[3/3] 测试 XSS…")
scanner.test_xss(
f"{TARGET}/api/search",
{'q': 'test', 'filter': 'all'}
)
# 生成报告
scanner.generate_report('./reports/dast/api-scan-report.json')
print("\\n✅ 扫描完成!")
4.5 运行时漏洞报告解析
创建一个报告解析脚本,将 DAST 结果转化为可操作的修复建议:
#!/usr/bin/env python3
"""
parse_dast_report.py – 解析 DAST 扫描报告并生成修复建议
"""
import json
import sys
from pathlib import Path
def parse_zap_report(report_path: str) –> dict:
"""解析 ZAP JSON 报告"""
with open(report_path, 'r') as f:
data = json.load(f)
alerts = data.get('alerts', [])
# 按 CWE 分类
cwe_mapping = {}
for alert in alerts:
cwe_id = alert.get('cweid', 'N/A')
if cwe_id not in cwe_mapping:
cwe_mapping[cwe_id] = []
cwe_mapping[cwe_id].append({
'name': alert.get('name'),
'risk': alert.get('risk'),
'url': alert.get('url'),
'description': alert.get('description', '')[:200],
'solution': alert.get('solution', '')[:200]
})
return cwe_mapping
def generate_fix_suggestions(cwe_mapping: dict) –> str:
"""生成修复建议"""
suggestions = []
suggestions.append("# DAST 扫描修复建议\\n")
suggestions.append(f"生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M')}\\n")
for cwe_id, alerts in cwe_mapping.items():
suggestions.append(f"\\n## CWE-{cwe_id}")
for alert in alerts:
suggestions.append(f"- **{alert['name']}** [{alert['risk']}]")
suggestions.append(f" – URL: {alert['url']}")
suggestions.append(f" – 修复: {alert['solution']}")
return '\\n'.join(suggestions)
if __name__ == '__main__':
if len(sys.argv) < 2:
print("用法: python3 parse_dast_report.py <report.json>")
sys.exit(1)
report_path = sys.argv[1]
cwe_mapping = parse_zap_report(report_path)
suggestions = generate_fix_suggestions(cwe_mapping)
# 输出到文件
output_path = './reports/dast/fix-suggestions.md'
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
f.write(suggestions)
print(f"修复建议已生成: {output_path}")
五、编写提示词引导 AI 识别常见漏洞模式
5.1 Cursor AI 安全审查提示词设计原则
在 Cursor 中,AI 可以成为强大的安全审查助手。设计有效的安全提示词需要遵循以下原则:
原则一:明确角色与目标
你是一位资深应用安全工程师,专注于代码安全审查…
原则二:提供具体上下文
请审查以下 Node.js Express 代码,关注 OWASP Top 10 漏洞…
原则三:指定输出格式
请以表格形式输出:漏洞类型 | 严重等级 | 代码位置 | 修复建议
原则四:限定范围避免幻觉
只报告你确定的安全问题,不要猜测或过度推断…
5.2 SQL 注入识别提示词模板
在 Cursor 中按 Ctrl+L 打开 AI 对话,使用以下提示词:
# 角色设定
你是一位专注于数据库安全的资深安全工程师,精通 SQL 注入攻击与防御。
# 任务
请审查以下代码,识别所有潜在的 SQL 注入漏洞。
# 审查要点
1. 是否存在字符串拼接构造 SQL 语句
2. 用户输入是否直接进入查询而未参数化
3. ORM 使用是否存在 raw query 注入风险
4. 存储过程调用是否安全
5. 动态表名/列名是否有白名单验证
# 输出格式
对每个发现的问题,请提供:
– 📍 位置:文件名 + 行号
– 🔴 严重等级:Critical/High/Medium/Low
– 📝 问题描述:具体说明漏洞原理
– 💡 修复方案:给出具体修复代码
– 📚 参考:相关 CWE 编号
# 注意事项
– 只报告确定的问题,不确定的标注为"需要人工确认"
– 如果使用了参数化查询,不要误报
– 考虑框架自带的防护机制(如 Sequelize 的参数化)
# 待审查代码
[在此粘贴代码或选中代码后发送]
5.3 XSS 漏洞检测提示词模板
# 角色设定
你是一位 Web 前端安全专家,精通各类 XSS 攻击向量与防御策略。
# 任务
审查以下前端/后端代码,识别所有 XSS(跨站脚本)漏洞。
# 审查维度
1. **反射型 XSS**:用户输入是否未经编码直接输出到 HTML
2. **存储型 XSS**:数据库存储的内容在展示时是否转义
3. **DOM 型 XSS**:前端 JS 是否直接使用 innerHTML/document.write
4. **事件处理**:onerror/onclick 等事件属性是否可注入
5. **URL 注入**:href/src 属性是否可被 javascript: 协议利用
# 特别关注
– React 中的 dangerouslySetInnerHTML
– Vue 中的 v-html 指令
– Angular 中的 [innerHTML] 绑定
– 模板引擎的 raw/unescaped 输出(如 {{{ }}} in Handlebars)
– URL 参数直接插入 DOM
# 输出要求
对每个发现:
1. 漏洞类型(反射型/存储型/DOM型)
2. 攻击向量示例
3. 影响范围评估
4. 具体修复代码(包含转义函数推荐)
5. 防御纵深建议(CSP 配置等)
5.4 认证与授权缺陷检测
# 角色设定
你是一位身份认证与访问控制安全专家。
# 任务
审查以下代码中的认证(Authentication)和授权(Authorization)实现。
# 检查清单
## 认证相关
– [ ] 密码是否使用 bcrypt/argon2 哈希(非 MD5/SHA1)
– [ ] 是否有暴力破解防护(速率限制/账户锁定)
– [ ] Session/Token 是否设置合理过期时间
– [ ] 登录失败是否返回统一错误信息(不暴露用户是否存在)
– [ ] 密码重置流程是否安全
## 授权相关
– [ ] 是否存在越权访问(水平/垂直)
– [ ] API 端点是否都有权限校验
– [ ] 是否存在 IDOR(不安全的直接对象引用)
– [ ] JWT 是否验证签名和过期时间
– [ ] 敏感操作是否有二次确认
## 会话管理
– [ ] Cookie 是否设置 HttpOnly/Secure/SameSite
– [ ] 是否有 CSRF 防护
– [ ] 登出是否真正销毁会话
# 输出
按 OWASP 分类输出,每个问题附带修复代码示例。
5.5 敏感信息泄露检测
# 角色设定
你是一位数据安全与隐私保护专家。
# 任务
审查代码中是否存在敏感信息泄露风险。
# 检查项
1. **硬编码凭证**:密码、API Key、Token 是否硬编码在源码中
2. **日志泄露**:日志中是否记录敏感数据(密码、身份证号、银行卡号)
3. **错误信息**:异常处理是否暴露系统内部信息(堆栈、路径、版本)
4. **前端泄露**:源代码/注释中是否包含内部 URL、测试账号
5. **配置泄露**:.env 文件是否被忽略、调试模式是否关闭
6. **响应泄露**:API 响应是否返回多余敏感字段
7. **版本信息**:是否暴露框架/库版本号
# 正则模式参考
– API Key: /[A-Za-z0-9_\\-]{32,}/
– AWS Key: /AKIA[0-9A-Z]{16}/
– 私钥: /—–BEGIN (RSA|EC) PRIVATE KEY—–/
– 密码字段: /password\\s*[:=]\\s*['"][^'"]+['"]/
# 输出
对每个发现标注:泄露类型、影响等级、修复方案、预防措施
5.6 提示词链组合策略
将多个安全提示词组合成完整的审查流程,形成系统化的安全分析链条:
完整安全审查提示词链(分步执行):
# 完整安全审查提示词链(在 Cursor 中按步骤执行)
## 第一步:快速扫描(Ctrl+L 打开 AI 对话)
"请快速扫描这段代码,列出所有潜在的安全问题,按严重性排序。
不需要详细解释,只需要列出问题类型和位置。格式如下:
1. [Critical] 文件:行号 – 问题简述
2. [High] 文件:行号 – 问题简述
…"
## 第二步:深入分析(针对第一步发现的问题)
"针对你发现的第 N 个问题 [具体问题],请详细分析:
1. 攻击者如何利用这个漏洞?给出具体的攻击步骤和 payload
2. 这个漏洞的影响范围有多大?
3. 请给出完整的修复代码
4. 如何验证修复是否有效?"
## 第三步:防御加固
"在修复上述问题的基础上,请建议额外的防御措施:
1. 需要添加哪些安全头?
2. 是否需要添加速率限制?
3. 日志记录应如何调整?
4. 是否需要添加额外的输入验证?"
## 第四步:回归验证
"请审查修复后的代码,确认:
1. 原始漏洞是否已完全修复?
2. 修复是否引入了新的安全问题?
3. 修复是否影响了正常业务逻辑?
4. 是否有遗漏的攻击向量?"
提示词链的 .cursorrules 配置:
将安全审查角色固化到项目配置中,创建项目根目录下的 .cursorrules 文件:
<!– .cursorrules – 项目级 AI 安全审查配置 –>
# 项目安全审查规则
## 角色定义
你是一位资深应用安全工程师,在辅助编码时始终关注安全性。
## 编码时的安全准则
1. 所有用户输入必须验证和净化
2. 数据库操作必须使用参数化查询
3. 禁止使用 eval()、exec() 等危险函数
4. 敏感数据必须加密存储
5. API 端点必须有认证和授权检查
6. 错误处理不得暴露内部信息
7. 日志中不得记录敏感数据
## 代码生成时的安全检查
当生成代码时,自动考虑:
– 输入验证是否完整
– 是否存在注入风险
– 错误处理是否安全
– 是否有适当的访问控制
– 依赖是否安全
## 审查输出格式
发现安全问题时,使用以下格式:
⚠️ [严重等级] 问题描述
📍 位置:文件:行号
💡 修复建议:具体方案
📚 参考:CWE/OWASP 编号
## 技术栈特定规则
– Express.js: 必须使用 helmet(),所有路由需要输入验证
– React: 禁止 dangerouslySetInnerHTML,使用 DOMPurify
– Python: 使用参数化查询,禁止 os.system()
– 数据库: 所有查询必须参数化,禁止字符串拼接
组合提示词实战示例:
# 场景:审查一个用户注册接口
## 提示词 1(初始扫描):
"审查以下 Express.js 用户注册代码,检查 OWASP Top 10 中的所有相关漏洞:
```javascript
app.post('/register', async (req, res) => {
const { username, email, password } = req.body;
const query = `INSERT INTO users (username, email, password)
VALUES ('${username}', '${email}', '${password}')`;
await db.execute(query);
res.send('注册成功');
});
```"
## 期望 AI 输出:
1. [Critical] SQL 注入 – 字符串拼接构造 SQL
2. [Critical] 明文密码存储 – 未使用哈希
3. [High] 缺少输入验证 – 未验证 email 格式
4. [Medium] 缺少速率限制 – 可被暴力注册
5. [Low] 响应信息过于简单 – 缺少适当状态码
## 提示词 2(深入修复):
"请给出完整的修复代码,包含:
1. 参数化查询
2. bcrypt 密码哈希
3. 输入验证(使用 express-validator)
4. 速率限制
5. 适当的错误处理"
六、实战演练:从代码编写到风险修复闭环
6.1 项目初始化与场景设定
本节通过一个完整的实战项目,演示从代码编写到漏洞发现、修复、验证的完整闭环。
场景设定:构建一个简单的博客系统 API,包含用户管理、文章发布、评论功能。
项目初始化:
# 创建项目目录
mkdir secure-blog-api && cd secure-blog-api
# 初始化 Node.js 项目
npm init -y
# 安装依赖
npm install express mysql2 bcrypt jsonwebtoken helmet cors express-validator
npm install –save-dev nodemon eslint eslint-plugin-security semgrep
# 创建项目结构
mkdir -p src/{routes,controllers,middleware,models,utils}
mkdir -p tests
mkdir -p semgrep-rules/{injection,xss,auth}
mkdir -p scripts
mkdir -p reports
项目结构:
secure-blog-api/
├── src/
│ ├── app.js # 应用入口
│ ├── routes/
│ │ ├── auth.js # 认证路由
│ │ ├── posts.js # 文章路由
│ │ └── comments.js # 评论路由
│ ├── controllers/
│ │ ├── authController.js
│ │ ├── postController.js
│ │ └── commentController.js
│ ├── middleware/
│ │ ├── auth.js # 认证中间件
│ │ ├── validate.js # 输入验证中间件
│ │ └── errorHandler.js # 错误处理中间件
│ ├── models/
│ │ └── db.js # 数据库连接
│ └── utils/
│ ├── sanitize.js # 输入净化工具
│ └── logger.js # 安全日志工具
├── semgrep-rules/ # 自定义安全规则
├── scripts/ # 自动化脚本
├── tests/ # 测试文件
├── reports/ # 扫描报告
├── .eslintrc.json
├── .cursorrules
├── .semgrepignore
└── package.json
6.2 编写含漏洞的示例代码
为了演示完整的安全工作流,我们先编写一段故意包含漏洞的代码(仅用于教学演示):
// src/controllers/authController.js
// ⚠️ 警告:此文件包含多个安全漏洞,仅用于教学演示!
// 实际项目中绝对不要这样编写代码!
const db = require('../models/db');
const jwt = require('jsonwebtoken');
// 漏洞1:硬编码密钥
const SECRET_KEY = "my-super-secret-key-12345";
// 漏洞2:SQL 注入 – 字符串拼接
exports.login = async (req, res) => {
const { username, password } = req.body;
// 危险:直接拼接用户输入到 SQL 语句
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
try {
const [rows] = await db.execute(query);
if (rows.length > 0) {
// 漏洞3:JWT 未设置过期时间
const token = jwt.sign({ userId: rows[0].id, role: rows[0].role }, SECRET_KEY);
// 漏洞4:返回过多用户信息
res.json({
success: true,
token: token,
user: rows[0] // 包含密码哈希等敏感信息
});
} else {
// 漏洞5:错误信息暴露用户是否存在
res.status(401).json({ error: '用户名不存在' });
}
} catch (error) {
// 漏洞6:暴露详细错误信息
res.status(500).json({ error: error.message, stack: error.stack });
}
};
// 漏洞7:缺少输入验证
exports.register = async (req, res) => {
const { username, email, password } = req.body;
// 漏洞8:明文存储密码
const query = `INSERT INTO users (username, email, password) VALUES ('${username}', '${email}', '${password}')`;
try {
await db.execute(query);
res.json({ success: true, message: '注册成功' });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// src/controllers/postController.js
// ⚠️ 同样包含安全漏洞的教学代码
const db = require('../models/db');
// 漏洞9:存储型 XSS – 未净化用户输入
exports.createPost = async (req, res) => {
const { title, content, authorId } = req.body;
// 直接将用户输入存入数据库,未做任何净化
const query = `INSERT INTO posts (title, content, author_id) VALUES (?, ?, ?)`;
try {
const [result] = await db.execute(query, [title, content, authorId]);
res.json({ success: true, postId: result.insertId });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 漏洞10:IDOR – 未验证资源所有权
exports.getPost = async (req, res) => {
const postId = req.params.id;
// 任何人都可以访问任何文章,未验证权限
const query = `SELECT * FROM posts WHERE id = ${postId}`;
try {
const [rows] = await db.execute(query);
if (rows.length > 0) {
// 漏洞11:直接输出未转义的 HTML 内容
res.json({ success: true, post: rows[0] });
} else {
res.status(404).json({ error: '文章不存在' });
}
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 漏洞12:批量赋值 – 未限制可更新字段
exports.updatePost = async (req, res) => {
const postId = req.params.id;
const updates = req.body; // 接受所有字段
// 构造动态 UPDATE 语句
const fields = Object.keys(updates).map(key => `${key} = '${updates[key]}'`).join(', ');
const query = `UPDATE posts SET ${fields} WHERE id = ${postId}`;
try {
await db.execute(query);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
};
// 漏洞13:命令注入
exports.exportPost = async (req, res) => {
const { format, filename } = req.query;
const { exec } = require('child_process');
// 危险:用户输入直接进入命令
exec(`pandoc posts/${filename} -o exports/${filename}.${format}`, (error, stdout) => {
if (error) {
res.status(500).json({ error: error.message });
} else {
res.download(`exports/${filename}.${format}`);
}
});
};
6.3 SAST 扫描触发与结果分析
在 Cursor 中触发扫描:
方法一:保存文件时自动触发(已配置 Semgrep 扩展)
方法二:手动在终端运行:
# 在 Cursor 终端中执行
# 扫描整个 src 目录
semgrep –config auto –config ./semgrep-rules/ ./src –json > reports/semgrep-scan.json
# 查看人类可读的结果
semgrep –config auto –config ./semgrep-rules/ ./src
预期扫描结果:
┌─────────────────────────────────────────────────────────────────┐
│ Semgrep 扫描结果 – secure-blog-api │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 🔴 ERROR: sql-injection-string-concat │
│ src/controllers/authController.js:14 │
│ > const query = `SELECT * FROM users WHERE username = … │
│ │
│ 🔴 ERROR: sql-injection-string-concat │
│ src/controllers/authController.js:30 │
│ > const query = `INSERT INTO users … │
│ │
│ 🔴 ERROR: hardcoded-secret │
│ src/controllers/authController.js:7 │
│ > const SECRET_KEY = "my-super-secret-key-12345"; │
│ │
│ 🔴 ERROR: plaintext-password-storage │
│ src/controllers/authController.js:30 │
│ > INSERT INTO users … password … │
│ │
│ 🔴 ERROR: command-injection │
│ src/controllers/postController.js:62 │
│ > exec(`pandoc posts/${filename}…`) │
│ │
│ 🟠 WARNING: missing-input-validation │
│ src/controllers/authController.js:27 │
│ > exports.register = async (req, res) => { │
│ │
│ 🟠 WARNING: excessive-data-exposure │
│ src/controllers/authController.js:19 │
│ > user: rows[0] │
│ │
│ 🟡 INFO: missing-rate-limiting │
│ src/controllers/authController.js:12 │
│ > exports.login = async (req, res) => { │
│ │
│ 总计: 5 ERROR, 2 WARNING, 1 INFO │
└─────────────────────────────────────────────────────────────────┘
6.4 AI 辅助漏洞解读
在 Cursor 中选中问题代码,按 Ctrl+L 打开 AI 对话:
提示词:
请解释这个 SQL 注入漏洞的攻击原理,并给出具体的攻击 payload 示例。
然后提供完整的修复代码。
AI 预期回复(示例):
## 漏洞分析:SQL 注入
### 攻击原理
当前代码使用模板字符串直接拼接用户输入:
```javascript
const query = `SELECT * FROM users WHERE username = '${username}' AND password = '${password}'`;
攻击者可以输入以下用户名:
admin' —
这使得 SQL 变为:
SELECT * FROM users WHERE username = 'admin' –' AND password = ''
— 注释掉了密码检查,攻击者无需密码即可登录。
-
更危险的 Payload
' UNION SELECT id, username, password, 'admin' FROM users —
可以获取所有用户数据。
-
修复代码 [完整修复代码见下一节]
6.5 修复代码编写与验证
修复后的 authController.js:
// src/controllers/authController.js
// ✅ 修复后的安全版本
const db = require('../models/db');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const { body, validationResult } = require('express-validator');
const { sanitizeInput } = require('../utils/sanitize');
const { logger } = require('../utils/logger');
// ✅ 修复1:密钥从环境变量读取
const SECRET_KEY = process.env.JWT_SECRET_KEY;
const SALT_ROUNDS = 12; // bcrypt 盐值轮数
// 输入验证规则
const loginValidation = [
body('username')
.trim()
.isLength({ min: 3, max: 50 })
.withMessage('用户名长度必须在3-50个字符之间')
.matches(/^[a-zA-Z0-9_]+$/)
.withMessage('用户名只能包含字母、数字和下划线'),
body('password')
.isLength({ min: 8, max: 128 })
.withMessage('密码长度必须在8-128个字符之间')
];
const registerValidation = [
body('username')
.trim()
.isLength({ min: 3, max: 50 })
.matches(/^[a-zA-Z0-9_]+$/),
body('email')
.trim()
.isEmail()
.withMessage('请输入有效的邮箱地址')
.normalizeEmail(),
body('password')
.isLength({ min: 8, max: 128 })
.matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&]).+$/)
.withMessage('密码必须包含大小写字母、数字和特殊字符')
];
/**
* 用户登录
* ✅ 修复2:使用参数化查询防止 SQL 注入
* ✅ 修复3:使用 bcrypt 验证密码哈希
* ✅ 修复4:JWT 设置过期时间
* ✅ 修复5:统一错误信息,不暴露用户是否存在
* ✅ 修复6:安全的错误处理
*/
exports.login = [
loginValidation,
async (req, res) => {
// 检查验证结果
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array().map(e => ({
field: e.path,
message: e.msg
}))
});
}
const { username, password } = req.body;
try {
// ✅ 参数化查询 – 防止 SQL 注入
const query = 'SELECT id, username, email, password_hash, role, created_at FROM users WHERE username = ?';
const [rows] = await db.execute(query, [username]);
if (rows.length === 0) {
// ✅ 统一错误信息 – 不暴露用户是否存在
logger.warn(`登录失败 – 用户不存在: ${sanitizeInput(username)}`);
return res.status(401).json({
success: false,
error: '用户名或密码错误' // 统一错误信息
});
}
const user = rows[0];
// ✅ 使用 bcrypt 比较密码哈希
const isPasswordValid = await bcrypt.compare(password, user.password_hash);
if (!isPasswordValid) {
logger.warn(`登录失败 – 密码错误: ${sanitizeInput(username)}`);
return res.status(401).json({
success: false,
error: '用户名或密码错误' // 与上面相同的错误信息
});
}
// ✅ JWT 设置过期时间(2小时)
const token = jwt.sign(
{
userId: user.id,
role: user.role
// 不包含敏感信息
},
SECRET_KEY,
{
expiresIn: '2h', // 2小时过期
algorithm: 'HS256'
}
);
// ✅ 只返回必要的用户信息
res.json({
success: true,
token: token,
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
// 不返回 password_hash 等敏感字段
}
});
logger.info(`用户登录成功: ${user.id}`);
} catch (error) {
// ✅ 安全的错误处理 – 不暴露内部信息
logger.error(`登录处理错误: ${error.message}`);
res.status(500).json({
success: false,
error: '服务器内部错误,请稍后重试'
});
}
}
];
/**
* 用户注册
* ✅ 修复7:完整的输入验证
* ✅ 修复8:bcrypt 哈希密码
*/
exports.register = [
registerValidation,
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array().map(e => ({
field: e.path,
message: e.msg
}))
});
}
const { username, email, password } = req.body;
try {
// 检查用户名是否已存在
const [existing] = await db.execute(
'SELECT id FROM users WHERE username = ? OR email = ?',
[username, email]
);
if (existing.length > 0) {
return res.status(409).json({
success: false,
error: '用户名或邮箱已被注册'
});
}
// ✅ 使用 bcrypt 哈希密码
const passwordHash = await bcrypt.hash(password, SALT_ROUNDS);
// ✅ 参数化查询
await db.execute(
'INSERT INTO users (username, email, password_hash) VALUES (?, ?, ?)',
[username, email, passwordHash]
);
logger.info(`新用户注册: ${username}`);
res.status(201).json({
success: true,
message: '注册成功'
});
} catch (error) {
logger.error(`注册处理错误: ${error.message}`);
res.status(500).json({
success: false,
error: '注册失败,请稍后重试'
});
}
}
];
修复后的 postController.js:
// src/controllers/postController.js
// ✅ 修复后的安全版本
const db = require('../models/db');
const DOMPurify = require('isomorphic-dompurify');
const { body, param, validationResult } = require('express-validator');
const { sanitizeInput } = require('../utils/sanitize');
const { logger } = require('../utils/logger');
const path = require('path');
// 允许更新的字段白名单
const ALLOWED_UPDATE_FIELDS = ['title', 'content', 'status'];
/**
* 创建文章
* ✅ 修复9:输入净化防止 XSS
*/
exports.createPost = [
body('title')
.trim()
.isLength({ min: 1, max: 200 })
.withMessage('标题长度必须在1-200个字符之间'),
body('content')
.trim()
.isLength({ min: 1, max: 50000 })
.withMessage('内容长度必须在1-50000个字符之间'),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
const { title, content } = req.body;
// 从 JWT token 中获取用户ID(而非请求体)
const authorId = req.user.userId;
try {
// ✅ 净化 HTML 内容防止存储型 XSS
const sanitizedContent = DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'code', 'pre'],
ALLOWED_ATTR: ['href', 'target', 'rel'],
// 禁止 javascript: 协议
ALLOW_DATA_ATTR: false
});
// ✅ 参数化查询
const [result] = await db.execute(
'INSERT INTO posts (title, content, author_id, created_at) VALUES (?, ?, ?, NOW())',
[sanitizeInput(title), sanitizedContent, authorId]
);
logger.info(`文章创建成功: ID=${result.insertId}, 作者=${authorId}`);
res.status(201).json({
success: true,
postId: result.insertId
});
} catch (error) {
logger.error(`创建文章失败: ${error.message}`);
res.status(500).json({
success: false,
error: '创建文章失败'
});
}
}
];
/**
* 获取文章
* ✅ 修复10:参数化查询 + 输入验证
*/
exports.getPost = [
param('id')
.isInt({ min: 1 })
.withMessage('无效的文章ID'),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
error: '无效的文章ID'
});
}
const postId = parseInt(req.params.id, 10);
try {
// ✅ 参数化查询
const [rows] = await db.execute(
'SELECT id, title, content, author_id, created_at, updated_at FROM posts WHERE id = ? AND status = ?',
[postId, 'published']
);
if (rows.length === 0) {
return res.status(404).json({
success: false,
error: '文章不存在'
});
}
res.json({
success: true,
post: rows[0]
});
} catch (error) {
logger.error(`获取文章失败: ${error.message}`);
res.status(500).json({
success: false,
error: '获取文章失败'
});
}
}
];
/**
* 更新文章
* ✅ 修复11:字段白名单 + 所有权验证
* ✅ 修复12:参数化动态查询
*/
exports.updatePost = [
param('id').isInt({ min: 1 }),
body('title').optional().trim().isLength({ min: 1, max: 200 }),
body('content').optional().trim().isLength({ min: 1, max: 50000 }),
body('status').optional().isIn(['draft', 'published', 'archived']),
async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
success: false,
errors: errors.array()
});
}
const postId = parseInt(req.params.id, 10);
const userId = req.user.userId;
try {
// ✅ 验证资源所有权
const [ownership] = await db.execute(
'SELECT id FROM posts WHERE id = ? AND author_id = ?',
[postId, userId]
);
if (ownership.length === 0) {
return res.status(403).json({
success: false,
error: '无权修改此文章'
});
}
// ✅ 字段白名单过滤
const updates = {};
for (const [key, value] of Object.entries(req.body)) {
if (ALLOWED_UPDATE_FIELDS.includes(key)) {
updates[key] = key === 'content' ? DOMPurify.sanitize(value) : sanitizeInput(value);
}
}
if (Object.keys(updates).length === 0) {
return res.status(400).json({
success: false,
error: '没有有效的更新字段'
});
}
// ✅ 安全的动态查询构建
const setClauses = Object.keys(updates).map(key => `${key} = ?`);
const values = Object.values(updates);
values.push(postId);
await db.execute(
`UPDATE posts SET ${setClauses.join(', ')}, updated_at = NOW() WHERE id = ?`,
values
);
logger.info(`文章更新: ID=${postId}, 用户=${userId}`);
res.json({ success: true });
} catch (error) {
logger.error(`更新文章失败: ${error.message}`);
res.status(500).json({
success: false,
error: '更新文章失败'
});
}
}
];
/**
* 导出文章
* ✅ 修复13:移除命令注入风险,使用安全的文件操作
*/
exports.exportPost = [
param('id').isInt({ min: 1 }),
async (req, res) => {
const postId = parseInt(req.params.id, 10);
const format = req.query.format;
// ✅ 白名单验证导出格式
const ALLOWED_FORMATS = ['pdf', 'html', 'markdown'];
if (!ALLOWED_FORMATS.includes(format)) {
return res.status(400).json({
success: false,
error: '不支持的导出格式'
});
}
try {
const [rows] = await db.execute(
'SELECT title, content FROM posts WHERE id = ?',
[postId]
);
if (rows.length === 0) {
return res.status(404).json({
success: false,
error: '文章不存在'
});
}
// ✅ 使用安全的文件生成方式,而非 shell 命令
const safeFilename = `post_${postId}_${Date.now()}.${format}`;
const outputPath = path.join(__dirname, '../../exports', safeFilename);
// 使用 Node.js 库生成文件(而非 exec 命令)
// 具体实现取决于格式…
res.json({
success: true,
downloadUrl: `/exports/${safeFilename}`
});
} catch (error) {
logger.error(`导出文章失败: ${error.message}`);
res.status(500).json({
success: false,
error: '导出失败'
});
}
}
];
6.6 回归扫描确认修复有效
修复完成后,重新运行安全扫描验证:
# 重新扫描
semgrep –config auto –config ./semgrep-rules/ ./src
# 期望输出:
# 无 ERROR 级别发现
# 可能仍有少量 INFO 级别建议
# 运行 ESLint 安全检查
npx eslint –ext .js ./src –rule 'security/detect-eval-with-expression: error'
# 运行 Bandit(如果有 Python 代码)
# bandit -r ./src
使用 Cursor AI 进行修复验证:
# 在 Cursor 中按 Ctrl+L,输入:
"请对比修复前后的代码,确认以下问题已解决:
1. SQL 注入 → 参数化查询 ✓
2. 明文密码 → bcrypt 哈希 ✓
3. 硬编码密钥 → 环境变量 ✓
4. 命令注入 → 安全文件操作 ✓
5. XSS → DOMPurify 净化 ✓
是否还有遗漏的安全问题?"
七、误报率优化策略与审计结果精准度提升
7.1 误报产生的常见原因
误报(False Positive)是 SAST 工具面临的最大挑战之一。常见原因包括:
| 缺乏上下文理解 | 测试代码中的"危险"操作 | ~30% |
| 规则过于宽泛 | 匹配所有字符串拼接 | ~25% |
| 框架特性未识别 | ORM 已自动参数化 | ~20% |
| 安全控制已存在 | 上层已做输入验证 | ~15% |
| 死代码/不可达路径 | 已废弃但未删除的代码 | ~10% |
7.2 规则精细化调优
策略一:添加上下文条件
# 优化前:过于宽泛的规则
rules:
– id: detect–string–concat–sql
pattern: |
$VAR = "…" + $INPUT + "…"
message: "可能的 SQL 注入"
languages: [javascript]
severity: ERROR
# 优化后:添加上下文限定
rules:
– id: detect–string–concat–sql
patterns:
– pattern: |
$VAR = "…" + $INPUT + "…"
# 限定变量名包含 SQL 关键词
– metavariable-regex:
metavariable: $VAR
regex: (?i).*(sql|query|stmt|select|insert|update|delete).*
# 排除已知的安全模式
– pattern-not: |
$VAR = "…" + parseInt($INPUT) + "…"
– pattern-not: |
$VAR = "…" + Number($INPUT) + "…"
# 排除测试文件
– pattern-not-inside: |
describe(…, () => { … })
– pattern-not-inside: |
test(…, () => { … })
message: "可能的 SQL 注入:检测到 SQL 查询字符串拼接"
languages: [javascript]
severity: ERROR
metadata:
confidence: HIGH # 提高置信度标注
策略二:使用 taint 模式替代简单模式匹配
# 使用污点追踪,只在数据确实从不可信源流向危险操作时报警
rules:
– id: taint–sql–injection–precise
mode: taint
pattern-sources:
# 明确定义不可信数据来源
– pattern: req.query.$PARAM
– pattern: req.body.$PARAM
– pattern: req.params.$PARAM
– pattern: req.headers.$PARAM
pattern-sinks:
# 明确定义危险操作
– pattern: db.query($SQL)
– pattern: db.execute($SQL)
– pattern: connection.query($SQL)
pattern-sanitizers:
# 定义净化操作(经过这些处理的数据是安全的)
– pattern: parseInt($X)
– pattern: Number($X)
– pattern: $X.replace(…)
– pattern: sanitize($X)
– pattern: escape($X)
# 参数化查询本身就是安全的
– pattern: db.query($SQL, [$PARAMS])
– pattern: db.execute($SQL, [$PARAMS])
message: >
用户输入 $SOURCE 未经净化直接流入 SQL 查询。
请使用参数化查询。
languages: [javascript, typescript]
severity: ERROR
metadata:
confidence: VERY_HIGH
cwe: "CWE-89"
7.3 上下文感知过滤
利用 Semgrep 的 pattern-not-inside 排除安全上下文:
rules:
– id: detect–exec–usage
patterns:
– pattern: exec($CMD)
# 排除:硬编码的命令字符串(无用户输入)
– pattern-not: exec("ls –la")
– pattern-not: exec("npm test")
– pattern-not: exec("git status")
# 排除:在测试文件中
– pattern-not-inside: |
// @test
…
– pattern-not-inside: |
describe(…)
# 排除:已有输入验证
– pattern-not-inside: |
if (whitelist.includes($CMD)) {
…
}
message: "exec() 调用需要确认命令来源安全"
languages: [javascript]
severity: WARNING # 降级为警告,需要人工确认
7.4 白名单与抑制机制
项目级白名单配置 (.semgrepignore):
# .semgrepignore – Semgrep 忽略文件
# 语法与 .gitignore 相同
# 忽略第三方依赖
node_modules/
vendor/
bower_components/
# 忽略构建产物
dist/
build/
out/
*.min.js
*.bundle.js
# 忽略测试文件(可选,根据团队策略)
# tests/
# *.test.js
# *.spec.js
# 忽略生成的代码
generated/
migrations/
*.generated.*
# 忽略特定规则对特定文件
# 格式: 规则ID 文件路径
# no-eval tests/helpers/eval-helper.js
代码内抑制注释:
// 当确认某个警告是误报时,可以使用抑制注释
// nosemgrep: detect-eval-usage
// 原因:此处 eval 仅用于解析受信任的内部配置,输入来源已验证
const config = eval(trustedInternalConfig);
// 或者使用行内抑制
const result = eval(expression); // nosemgrep: detect-eval-usage
团队级白名单管理(创建 security-exceptions.yaml):
# security-exceptions.yaml – 安全例外管理
# 所有例外必须记录原因和审批人
exceptions:
– rule_id: detect–eval–usage
file: src/utils/template–engine.js
line: 42
reason: "内部模板引擎需要,输入已通过白名单验证"
approved_by: "security-team@company.com"
approved_date: "2026-07-15"
review_date: "2026-10-15" # 3个月后需重新评审
ticket: "SEC-2026-042"
– rule_id: hardcoded–secret
file: tests/fixtures/test–data.js
line: 15
reason: "测试用假数据,非真实凭证"
approved_by: "dev-lead@company.com"
approved_date: "2026-07-20"
review_date: null # 测试数据无需重新评审
7.5 持续反馈循环建立
#!/bin/bash
# scripts/feedback-loop.sh – 误报反馈收集脚本
# 定期运行,收集团队对扫描结果的反馈
REPORT_FILE="./reports/semgrep-scan.json"
FEEDBACK_FILE="./reports/feedback-log.json"
echo "=== 安全扫描反馈收集 ==="
echo ""
# 解析当前扫描结果
FINDINGS=$(cat ${REPORT_FILE} | python3 -c "
import json, sys
data = json.load(sys.stdin)
results = data.get('results', [])
print(f'当前发现: {len(results)} 个问题')
for r in results:
print(f' [{r[\\"extra\\"][\\"severity\\"]}] {r[\\"check_id\\"]} @ {r[\\"path\\"]}:{r[\\"start\\"][\\"line\\"]}')
")
echo "${FINDINGS}"
echo ""
# 记录到反馈日志
python3 << 'EOF'
import json
from datetime import datetime
feedback_entry = {
"timestamp": datetime.now().isoformat(),
"total_findings": 0,
"confirmed_true_positives": 0,
"confirmed_false_positives": 0,
"needs_review": 0,
"notes": ""
}
# 这里可以集成到团队的反馈系统
print("请将此数据同步到团队安全仪表板")
EOF
八、团队协作规范与安全检查流程标准化
8.1 安全编码规范文档化
创建团队安全编码规范文档 SECURITY_GUIDELINES.md:
# 团队安全编码规范 v2.0
## 1. 总则
– 所有代码必须通过 SAST 扫描后方可合并
– 安全漏洞修复优先级高于新功能开发
– 每位开发者每季度至少完成一次安全培训
## 2. 输入处理规范
### 2.1 所有用户输入必须验证
– 类型检查
– 长度限制
– 格式验证(正则)
– 范围检查
### 2.2 输出编码
– HTML 上下文:HTML 实体编码
– JavaScript 上下文:JS 转义
– URL 上下文:URL 编码
– CSS 上下文:CSS 转义
## 3. 数据库操作规范
– 必须使用参数化查询
– 禁止字符串拼接 SQL
– ORM raw query 需要特别审批
## 4. 认证与授权
– 密码必须使用 bcrypt (cost >= 12) 或 argon2
– JWT 必须设置过期时间
– 所有 API 必须有权限校验
## 5. 错误处理
– 生产环境禁止输出堆栈信息
– 统一错误响应格式
– 详细错误只记录到日志
## 6. 日志规范
– 禁止记录:密码、token、身份证号、银行卡号
– 必须记录:安全事件、认证失败、权限变更
8.2 Git Hooks 集成安全扫描
创建 .husky/pre-commit 和 .husky/pre-push:
#!/bin/sh
# .husky/pre-commit – 提交前安全检查
# 安装: npm install –save-dev husky && npx husky init
echo "🔒 运行提交前安全检查…"
# 1. 获取暂存的文件
STAGED_FILES=$(git diff –cached –name-only –diff-filter=ACM | grep -E '\\.(js|ts|jsx|tsx|py|java)$')
if [ -z "$STAGED_FILES" ]; then
echo "✅ 无需要检查的代码文件"
exit 0
fi
# 2. 运行 Semgrep 扫描暂存文件
echo "📋 Semgrep 扫描…"
echo "$STAGED_FILES" | xargs semgrep –config auto –config ./semgrep-rules/ –error
if [ $? -ne 0 ]; then
echo ""
echo "❌ 安全检查未通过!"
echo " 请修复上述安全问题后重新提交。"
echo " 如确认为误报,请联系安全团队添加例外。"
exit 1
fi
# 3. 检查是否包含敏感信息
echo "🔍 检查敏感信息…"
echo "$STAGED_FILES" | xargs grep -l -E "(password|secret|api_key|token)\\s*[:=]\\s*['\\"][^'\\"]{8,}['\\"]" 2>/dev/null
if [ $? -eq 0 ]; then
echo ""
echo "⚠️ 警告:检测到可能的硬编码敏感信息!"
echo " 请确认是否为真实凭证。"
read -p " 是否继续提交?(y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
fi
# 4. ESLint 安全检查
echo "📝 ESLint 安全检查…"
echo "$STAGED_FILES" | grep -E '\\.(js|ts|jsx|tsx)$' | xargs npx eslint –no-eslintrc –plugin security –rule 'security/detect-eval-with-expression: error' 2>/dev/null
echo ""
echo "✅ 安全检查通过!"
#!/bin/sh
# .husky/pre-push – 推送前完整安全扫描
echo "🔒 运行推送前完整安全扫描…"
# 完整项目扫描
semgrep –config auto –config ./semgrep-rules/ . –json > ./reports/pre-push-scan.json
# 检查是否有 ERROR 级别发现
ERROR_COUNT=$(python3 -c "
import json
with open('./reports/pre-push-scan.json') as f:
data = json.load(f)
errors = [r for r in data.get('results', []) if r['extra']['severity'] == 'ERROR']
print(len(errors))
")
if [ "$ERROR_COUNT" -gt 0 ]; then
echo "❌ 发现 ${ERROR_COUNT} 个高危安全问题,禁止推送!"
echo " 详细报告: ./reports/pre-push-scan.json"
exit 1
fi
echo "✅ 推送前安全检查通过"
8.3 Code Review 安全检查清单
# Code Review 安全检查清单
## 审查人:________ 日期:________ PR:________
### 输入验证
– [ ] 所有外部输入(请求参数、文件上传、第三方回调)是否已验证?
– [ ] 是否对输入进行了类型、长度、格式、范围检查?
– [ ] 文件上传是否验证了类型和大小?
### SQL/数据库
– [ ] 是否使用参数化查询?
– [ ] 是否存在动态 SQL 拼接?
– [ ] 数据库错误信息是否暴露给用户?
### 认证/授权
– [ ] 新端点是否有适当的认证检查?
– [ ] 是否验证了资源所有权(防 IDOR)?
– [ ] 权限检查是否在服务端(而非仅前端)?
### XSS 防护
– [ ] 用户输入在输出时是否进行了编码/转义?
– [ ] 是否使用了 dangerouslySetInnerHTML/v-html?
– [ ] CSP 策略是否覆盖新页面?
### 敏感数据
– [ ] 是否有硬编码的密钥/密码?
– [ ] 日志中是否记录了敏感信息?
– [ ] 敏感数据是否加密存储/传输?
### 错误处理
– [ ] 异常是否被正确捕获?
– [ ] 错误响应是否暴露了内部信息?
– [ ] 是否有适当的错误日志记录?
### 依赖安全
– [ ] 是否引入了新的依赖?
– [ ] 新依赖是否有已知漏洞?(npm audit / pip audit)
### 审查结论
– [ ] ✅ 通过,无安全问题
– [ ] ⚠️ 有条件通过,需修复后合并
– [ ] ❌ 不通过,存在严重安全问题
审查人签名:________
8.4 安全事件响应流程
# security-incident-response.yaml – 安全事件响应流程
severity_levels:
critical:
description: "数据泄露、RCE、认证完全绕过"
response_time: "1小时内响应"
actions:
– 立即通知安全团队负责人
– 评估影响范围
– 必要时下线受影响服务
– 24小时内完成修复
– 事后编写 RCA 报告
high:
description: "SQL注入、XSS、越权访问"
response_time: "4小时内响应"
actions:
– 通知安全团队
– 创建高优先级修复任务
– 3个工作日内完成修复
– 添加回归测试
medium:
description: "信息泄露、CSRF、配置不当"
response_time: "1个工作日内响应"
actions:
– 记录到安全任务看板
– 当前迭代内修复
– Code Review 确认修复
low:
description: "最佳实践建议、低风险配置"
response_time: "下一迭代处理"
actions:
– 记录到技术债务列表
– 规划修复时间
escalation_path:
– level: 1
contact: "开发团队负责人"
when: "初次发现"
– level: 2
contact: "安全团队"
when: "确认为真实漏洞"
– level: 3
contact: "CTO/安全总监"
when: "Critical 级别或影响用户数据"
– level: 4
contact: "法务/公关"
when: "涉及数据泄露需外部通报"
8.5 团队安全培训与知识共享
# 团队安全培训计划
## 新人入职(第1周)
– [ ] 安全编码规范培训(2小时)
– [ ] Cursor 安全工具链使用培训(1小时)
– [ ] 完成安全编码练习(含故意漏洞的代码修复)
– [ ] 阅读 OWASP Top 10 文档
## 季度培训
– [ ] 最新安全威胁趋势分享
– [ ] 内部安全事件复盘
– [ ] 新工具/规则更新培训
– [ ] CTF 练习或安全竞赛
## 知识共享机制
– 每周安全 Tip(Slack/飞书频道)
– 月度安全 Review 会议
– 内部安全 Wiki 维护
– 漏洞修复案例库积累
九、典型报错分析与插件链运行故障排查
9.1 插件安装失败排查
问题1:Semgrep CLI 安装失败
# 错误信息
ERROR: Could not find a version that satisfies the requirement semgrep
ERROR: No matching distribution found for semgrep
# 原因分析
# 1. Python 版本过低(需要 3.8+)
# 2. pip 版本过旧
# 3. 网络问题
# 解决方案
python3 –version # 确认 >= 3.8
# 升级 pip
pip3 install –upgrade pip
# 使用国内镜像(中国大陆用户)
pip3 install semgrep -i https://pypi.tuna.tsinghua.edu.cn/simple
# 如果仍然失败,使用 Docker
docker pull semgrep/semgrep:latest
alias semgrep='docker run –rm -v "${PWD}:/src" semgrep/semgrep semgrep'
问题2:Cursor 扩展市场无法访问
# 错误信息
Unable to connect to the marketplace
# 解决方案
# 1. 检查网络连接
curl -I https://marketplace.visualstudio.com
# 2. 配置代理(如果需要)
# 在 Cursor 设置中:
# "http.proxy": "http://127.0.0.1:7890"
# "http.proxyStrictSSL": false
# 3. 手动安装 VSIX
# 下载 .vsix 文件后:
# Ctrl+Shift+P → "Install from VSIX…"
问题3:Node.js 版本不兼容
# 错误信息
Error: ESLint requires Node.js version >= 18.0.0
# 解决方案
node –version # 检查当前版本
# 使用 nvm 切换版本
nvm install 18
nvm use 18
nvm alias default 18
# 重新安装依赖
rm -rf node_modules package-lock.json
npm install
9.2 规则解析错误
问题:YAML 语法错误
# 错误信息
semgrep.exceptions.SemgrepError:
Rule file semgrep-rules/custom.yaml is not valid YAML:
mapping values are not allowed here
# 常见原因及修复
# ❌ 错误:多行字符串未正确缩进
rules:
– id: test-rule
pattern: |
const x = "hello" # 错误:pattern 内容需要缩进
message: "test"
# ✅ 正确:
rules:
– id: test-rule
pattern: |
const x = "hello"
message: "test"
# ❌ 错误:特殊字符未转义
rules:
– id: test
message: "Use "safe" function" # 引号冲突
# ✅ 正确:
rules:
– id: test
message: 'Use "safe" function' # 使用单引号包裹
# 或者
message: >
Use "safe" function
验证规则文件的命令:
# 验证 YAML 语法
python3 -c "import yaml; yaml.safe_load(open('semgrep-rules/custom.yaml'))"
# 验证 Semgrep 规则
semgrep –config semgrep-rules/custom.yaml –validate
# 详细错误输出
semgrep –config semgrep-rules/custom.yaml –debug 2>&1 | head -50
9.3 扫描超时与性能问题
问题:大型项目扫描时间过长
# 优化方案
# 1. 使用 .semgrepignore 排除不需要扫描的目录
# 参考 7.4 节的 .semgrepignore 配置
# 2. 限制扫描的文件类型
semgrep –config auto –include="*.js" –include="*.ts" ./src
# 3. 使用并行扫描
semgrep –config auto –jobs 4 ./src
# 4. 只扫描变更文件(CI 环境)
git diff –name-only HEAD~1 | xargs semgrep –config auto
# 5. 使用缓存
semgrep –config auto –use-cache ./src
# 6. 分模块扫描(超大项目)
semgrep –config ./semgrep-rules/injection/ ./src/api/
semgrep –config ./semgrep-rules/xss/ ./src/frontend/
Cursor 中的性能优化配置:
{
// 减少实时扫描频率
"semgrep.scan.onType": false, // 关闭输入时扫描
"semgrep.scan.onSave": true, // 仅保存时扫描
// 限制扫描范围
"semgrep.exclude": [
"node_modules",
"dist",
"build",
".git",
"coverage",
"*.min.js",
"*.map",
"vendor",
"third-party"
],
// 增加内存限制
"semgrep.maxMemory": "4096",
// 超时设置
"semgrep.timeout": 300
}
9.4 AI 提示词无响应处理
问题:Cursor AI 对话无响应或响应不完整
# 排查步骤
## 1. 检查网络连接
– 确认能访问 AI 服务
– 检查防火墙/代理设置
## 2. 检查订阅状态
– 确认 Cursor Pro/Team 订阅有效
– 检查是否达到使用限额
## 3. 优化提示词
– 减少单次输入的代码量(建议 < 500 行)
– 分步骤提问而非一次性大段输入
– 明确指定输出格式
## 4. 替代方案
– 使用 Ctrl+K(内联编辑)代替 Ctrl+L(对话)
– 选中特定代码段再提问
– 重启 Cursor 后重试
9.5 网络代理与证书问题
# 问题:企业内网环境下的 SSL 证书错误
# 错误信息
ERROR: SSL: CERTIFICATE_VERIFY_FAILED
# 解决方案
# 方案1:配置企业 CA 证书
export SSL_CERT_FILE=/path/to/company-ca.pem
export REQUESTS_CA_BUNDLE=/path/to/company-ca.pem
# 方案2:pip 配置
pip3 config set global.cert /path/to/company-ca.pem
# 方案3:Node.js 环境
export NODE_EXTRA_CA_CERTS=/path/to/company-ca.pem
# 方案4:临时禁用验证(仅限开发环境,不推荐生产使用)
export NODE_TLS_REJECT_UNAUTHORIZED=0 # ⚠️ 仅用于调试
9.6 版本冲突解决
# 问题:多个安全工具版本冲突
# 诊断
pip3 list | grep -i semgrep
npm list eslint
python3 –version
node –version
# 解决方案:使用虚拟环境隔离
# Python 虚拟环境
python3 -m venv .security-tools
source .security-tools/bin/activate # Linux/macOS
# .security-tools\\Scripts\\activate # Windows
pip install semgrep bandit
# Node.js 使用项目本地版本
# 在 package.json 中锁定版本
{
"devDependencies": {
"eslint": "^9.0.0",
"eslint-plugin-security": "^3.0.0"
}
}
# 使用 npx 确保使用项目本地版本
npx eslint –version
故障排查决策树:
插件/工具不工作
├── 是否能正常安装?
│ ├── 否 → 检查网络/权限/版本要求(→ 9.1)
│ └── 是 → 继续
├── 是否能正常启动?
│ ├── 否 → 检查依赖/环境变量(→ 9.5, 9.6)
│ └── 是 → 继续
├── 扫描是否有输出?
│ ├── 无输出 → 检查配置/路径/权限
│ ├── 报错 → 检查规则语法(→ 9.2)
│ └── 超时 → 优化性能(→ 9.3)
└── 结果是否准确?
├── 误报多 → 优化规则(→ 第七章)
└── 漏报 → 补充规则/启用更多规则集
十、进阶技巧:扩展私有规则库与持续集成对接
10.1 企业私有规则库搭建
私有规则库目录结构:
company-security-rules/
├── README.md
├── CHANGELOG.md
├── version.yaml # 版本信息
├── rules/
│ ├── language-specific/
│ │ ├── javascript/
│ │ │ ├── injection.yaml
│ │ │ ├── xss.yaml
│ │ │ └── auth.yaml
│ │ ├── python/
│ │ │ ├── injection.yaml
│ │ │ └── deserialization.yaml
│ │ └── java/
│ │ ├── injection.yaml
│ │ └── crypto.yaml
│ ├── framework-specific/
│ │ ├── express/
│ │ ├── django/
│ │ ├── spring/
│ │ └── react/
│ ├── business-specific/
│ │ ├── payment-security.yaml # 支付安全
│ │ ├── pii-handling.yaml # 个人信息处理
│ │ └── api-standards.yaml # 内部 API 规范
│ └── compliance/
│ ├── gdpr.yaml # GDPR 合规
│ ├── pci-dss.yaml # PCI DSS 合规
│ └── soc2.yaml # SOC2 合规
├── tests/ # 规则测试用例
│ ├── javascript/
│ ├── python/
│ └── java/
└── scripts/
├── validate-rules.sh # 规则验证脚本
├── publish.sh # 发布脚本
└── update-registry.sh # 更新注册表
私有规则示例(业务特定):
# rules/business-specific/payment-security.yaml
# 支付安全专用规则
rules:
# 规则1:禁止在日志中记录卡号
– id: no–card–number–in–logs
patterns:
– pattern-either:
– pattern: |
logger.$METHOD(…, $CARD, …)
– pattern: |
console.log(…, $CARD, …)
– metavariable-regex:
metavariable: $CARD
regex: .*(card|cardNumber|card_number|pan|creditCard).*
message: >
🚫 PCI DSS 违规:禁止在日志中记录银行卡号。
请使用脱敏处理:仅保留前6后4位。
示例: maskCardNumber(cardNumber) → "6222****1234"
languages: [javascript, typescript, python, java]
severity: ERROR
metadata:
category: compliance
standard: PCI–DSS
requirement: "Requirement 3.4"
cwe: "CWE-532"
# 规则2:金额计算必须使用整数(分为单位)
– id: no–float–for–money
patterns:
– pattern-either:
– pattern: |
$AMOUNT = $X * $Y
– pattern: |
$AMOUNT = $X + $Y
– pattern: |
$AMOUNT = $X – $Y
– metavariable-regex:
metavariable: $AMOUNT
regex: .*(price|amount|total|fee|cost).*
– pattern-not: |
$AMOUNT = Math.round(…)
– pattern-not: |
$AMOUNT = parseInt(…)
message: >
⚠️ 金融计算不应使用浮点数。
请使用整数(以分/厘为单位)或专用库(如 decimal.js、BigDecimal)。
languages: [javascript, typescript]
severity: WARNING
metadata:
category: business–logic
confidence: MEDIUM
# 规则3:支付接口必须有幂等性检查
– id: payment–endpoint–needs–idempotency
patterns:
– pattern: |
$ROUTER.post("$PATH", …, $HANDLER)
– metavariable-regex:
metavariable: $PATH
regex: .*(pay|payment|charge|transaction|order).*
– pattern-not-inside: |
// @idempotent
…
message: >
支付相关接口必须实现幂等性。
请添加幂等性检查(如请求去重、状态机验证)。
添加 "// @idempotent" 注释标记已处理。
languages: [javascript, typescript]
severity: WARNING
metadata:
category: business–logic
规则库版本管理:
# version.yaml
version: "2.3.1"
last_updated: "2026-08-01"
maintainer: "security-team@company.com"
changelog:
– version: "2.3.1"
date: "2026-08-01"
changes:
– "新增 React 19 安全规则"
– "优化 SQL 注入检测精确度"
– version: "2.3.0"
date: "2026-07-15"
changes:
– "新增 GDPR 合规规则集"
– "支持 TypeScript 5.5 语法"
10.2 CI/CD 管道集成
GitHub Actions 配置:
# .github/workflows/security-scan.yml
name: Security Scan Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
sast-scan:
name: SAST 静态安全扫描
runs-on: ubuntu–latest
steps:
– name: 检出代码
uses: actions/checkout@v4
with:
fetch-depth: 0 # 完整历史,用于增量扫描
– name: 设置 Python
uses: actions/setup–python@v5
with:
python-version: '3.11'
– name: 安装 Semgrep
run: pip install semgrep
– name: 运行 Semgrep 扫描
run: |
semgrep \\
–config auto \\
–config ./semgrep-rules/ \\
–config ./company-security-rules/rules/ \\
–json \\
–output reports/semgrep-results.json \\
–sarif \\
–output reports/semgrep-results.sarif \\
.
continue-on-error: true # 不因扫描失败阻塞管道
– name: 检查扫描结果
run: |
ERROR_COUNT=$(python3 -c "
import json
with open('reports/semgrep-results.json') as f:
data = json.load(f)
errors = [r for r in data.get('results', [])
if r['extra']['severity'] == 'ERROR']
print(len(errors))
")
echo "发现 ${ERROR_COUNT} 个高危问题"
if [ "$ERROR_COUNT" –gt 0 ]; then
echo "::error::发现 ${ERROR_COUNT} 个高危安全漏洞,请修复后重新提交"
exit 1
fi
– name: 上传 SARIF 报告
uses: github/codeql–action/upload–sarif@v3
if: always()
with:
sarif_file: reports/semgrep–results.sarif
– name: 上传扫描报告
uses: actions/upload–artifact@v4
if: always()
with:
name: security–scan–report
path: reports/
dependency-scan:
name: 依赖安全扫描
runs-on: ubuntu–latest
steps:
– uses: actions/checkout@v4
– name: npm audit
run: |
npm audit –audit-level=high –json > reports/npm-audit.json || true
HIGH_VULNS=$(cat reports/npm-audit.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
print(data.get('metadata', {}).get('vulnerabilities', {}).get('high', 0) +
data.get('metadata', {}).get('vulnerabilities', {}).get('critical', 0))
")
echo "高危依赖漏洞: ${HIGH_VULNS}"
if [ "$HIGH_VULNS" -gt 0 ]; then
echo "::warning::发现 ${HIGH_VULNS} 个高危依赖漏洞"
fi
dast-scan:
name: DAST 动态安全扫描
runs-on: ubuntu–latest
needs: [sast–scan] # SAST 通过后才运行 DAST
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
– uses: actions/checkout@v4
– name: 启动应用
run: |
npm ci
npm run build
npm start &
sleep 10 # 等待应用启动
– name: 运行 ZAP 基线扫描
uses: zaproxy/action–baseline@v0.12.0
with:
target: 'http://localhost:3000'
rules_file_name: 'zap-rules.conf'
cmd_options: '-a -T 10'
– name: 上传 DAST 报告
uses: actions/upload–artifact@v4
if: always()
with:
name: dast–report
path: report_html.html
GitLab CI 配置:
# .gitlab-ci.yml 安全扫描部分
stages:
– security
sast:
stage: security
image: semgrep/semgrep:latest
script:
– semgrep ––config auto ––config ./semgrep–rules/ ––json ––output gl–sast–report.json .
artifacts:
reports:
sast: gl–sast–report.json
expire_in: 30 days
rules:
– if: $CI_MERGE_REQUEST_ID
– if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
dependency_scanning:
stage: security
image: node:20
script:
– npm audit ––json > gl–dependency–report.json || true
artifacts:
paths:
– gl–dependency–report.json
expire_in: 30 days
10.3 安全报告自动生成
完成安全报告自动生成器的实现:
#!/usr/bin/env python3
"""
generate_security_report.py – 自动生成安全扫描综合报告(完整版)
整合 SAST、DAST、依赖扫描结果,生成统一 HTML/Markdown 报告
用法: python3 scripts/generate_security_report.py [–format html|markdown|json]
输出: reports/security-report-{date}.html
"""
import json
import os
import sys
import argparse
from datetime import datetime
from pathlib import Path
from typing import List, Dict, Optional
class SecurityReportGenerator:
"""安全报告生成器 – 整合多种扫描源"""
def __init__(self, project_name: str):
"""
初始化报告生成器
Args:
project_name: 项目名称,用于报告标题
"""
self.project_name = project_name
self.timestamp = datetime.now()
self.data = {
'sast_findings': [], # SAST 静态扫描发现
'dast_findings': [], # DAST 动态扫描发现
'dependency_vulns': [], # 依赖漏洞
'ai_review_notes': [], # AI 审查备注
'summary': {} # 统计摘要
}
def load_semgrep_results(self, path: str) –> None:
"""
加载 Semgrep SAST 扫描结果
Args:
path: Semgrep JSON 报告文件路径
"""
if not os.path.exists(path):
print(f" ⚠️ SAST 报告不存在: {path},跳过")
return
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
results = data.get('results', [])
print(f" 📋 加载 SAST 发现: {len(results)} 条")
for result in results:
extra = result.get('extra', {})
metadata = extra.get('metadata', {})
self.data['sast_findings'].append({
'rule_id': result.get('check_id', 'unknown'),
'severity': extra.get('severity', 'INFO'),
'message': extra.get('message', '').strip(),
'file': result.get('path', ''),
'line': result.get('start', {}).get('line', 0),
'column': result.get('start', {}).get('col', 0),
'code_snippet': extra.get('lines', ''),
'cwe': metadata.get('cwe', 'N/A'),
'owasp': metadata.get('owasp', 'N/A'),
'confidence': metadata.get('confidence', 'MEDIUM'),
'fix_suggestion': extra.get('fix', '')
})
def load_zap_results(self, path: str) –> None:
"""
加载 OWASP ZAP DAST 扫描结果
Args:
path: ZAP JSON 报告文件路径
"""
if not os.path.exists(path):
print(f" ⚠️ DAST 报告不存在: {path},跳过")
return
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
alerts = data.get('alerts', [])
print(f" 📋 加载 DAST 发现: {len(alerts)} 条")
for alert in alerts:
self.data['dast_findings'].append({
'name': alert.get('name', ''),
'risk': alert.get('risk', 'Informational'),
'confidence': alert.get('confidence', ''),
'url': alert.get('url', ''),
'method': alert.get('method', 'GET'),
'param': alert.get('param', ''),
'description': alert.get('description', '')[:500],
'solution': alert.get('solution', '')[:500],
'cwe_id': alert.get('cweid', 'N/A'),
'wasc_id': alert.get('wascid', 'N/A'),
'evidence': alert.get('evidence', '')[:200]
})
def load_npm_audit(self, path: str) –> None:
"""
加载 npm audit 依赖扫描结果
Args:
path: npm audit JSON 输出文件路径
"""
if not os.path.exists(path):
print(f" ⚠️ 依赖审计报告不存在: {path},跳过")
return
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
# npm audit 格式(npm v7+)
vulns = data.get('vulnerabilities', {})
print(f" 📋 加载依赖漏洞: {len(vulns)} 个")
for pkg_name, vuln_info in vulns.items():
self.data['dependency_vulns'].append({
'package': pkg_name,
'severity': vuln_info.get('severity', 'unknown'),
'via': vuln_info.get('via', []),
'range': vuln_info.get('range', ''),
'fix_available': vuln_info.get('fixAvailable', False),
'direct': vuln_info.get('isDirect', False)
})
def generate_summary(self) –> None:
"""生成统计摘要数据"""
# SAST 按严重等级统计
sast_by_severity = {'ERROR': 0, 'WARNING': 0, 'INFO': 0}
for f in self.data['sast_findings']:
sev = f['severity']
if sev in sast_by_severity:
sast_by_severity[sev] += 1
# DAST 按风险等级统计
dast_by_risk = {'High': 0, 'Medium': 0, 'Low': 0, 'Informational': 0}
for f in self.data['dast_findings']:
risk = f['risk']
if risk in dast_by_risk:
dast_by_risk[risk] += 1
# 依赖漏洞按严重等级统计
dep_by_severity = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0}
for v in self.data['dependency_vulns']:
sev = v['severity']
if sev in dep_by_severity:
dep_by_severity[sev] += 1
self.data['summary'] = {
'project': self.project_name,
'scan_date': self.timestamp.strftime('%Y-%m-%d %H:%M:%S'),
'total_issues': (
len(self.data['sast_findings']) +
len(self.data['dast_findings']) +
len(self.data['dependency_vulns'])
),
'sast_total': len(self.data['sast_findings']),
'sast_by_severity': sast_by_severity,
'dast_total': len(self.data['dast_findings']),
'dast_by_risk': dast_by_risk,
'dependency_total': len(self.data['dependency_vulns']),
'dependency_by_severity': dep_by_severity,
'risk_score': self._calculate_risk_score()
}
def _calculate_risk_score(self) –> int:
"""
计算综合风险评分(0-100)
评分规则:
– Critical/ERROR: 每个 +15 分
– High: 每个 +10 分
– Medium/WARNING: 每个 +5 分
– Low/INFO: 每个 +1 分
最终分数上限 100
"""
score = 0
# SAST 计分
for f in self.data['sast_findings']:
if f['severity'] == 'ERROR':
score += 15
elif f['severity'] == 'WARNING':
score += 5
else:
score += 1
# DAST 计分
for f in self.data['dast_findings']:
if f['risk'] == 'High':
score += 10
elif f['risk'] == 'Medium':
score += 5
elif f['risk'] == 'Low':
score += 2
else:
score += 1
# 依赖漏洞计分
for v in self.data['dependency_vulns']:
if v['severity'] == 'critical':
score += 15
elif v['severity'] == 'high':
score += 10
elif v['severity'] == 'medium':
score += 5
else:
score += 1
return min(score, 100)
def generate_html_report(self, output_path: str) –> None:
"""
生成 HTML 格式的安全报告
Args:
output_path: 输出文件路径
"""
summary = self.data['summary']
# 构建高危发现列表
critical_findings = []
for f in self.data['sast_findings']:
if f['severity'] == 'ERROR':
critical_findings.append({
'type': 'SAST',
'title': f['rule_id'],
'location': f"{f['file']}:{f['line']}",
'message': f['message'],
'cwe': f['cwe']
})
for f in self.data['dast_findings']:
if f['risk'] == 'High':
critical_findings.append({
'type': 'DAST',
'title': f['name'],
'location': f['url'],
'message': f['description'][:200],
'cwe': f"CWE-{f['cwe_id']}"
})
# 生成 HTML
html_content = f"""<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>安全扫描报告 – {self.project_name}</title>
<style>
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f0f2f5;
color: #333;
line-height: 1.6;
}}
.container {{
max-width: 1200px;
margin: 0 auto;
padding: 40px 20px;
}}
.header {{
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: white;
padding: 40px;
border-radius: 12px;
margin-bottom: 30px;
}}
.header h1 {{ font-size: 28px; margin-bottom: 10px; }}
.header .meta {{ opacity: 0.8; font-size: 14px; }}
.risk-score {{
display: inline-block;
padding: 8px 20px;
border-radius: 20px;
font-weight: bold;
font-size: 18px;
margin-top: 15px;
}}
.risk-critical {{ background: #e74c3c; }}
.risk-high {{ background: #e67e22; }}
.risk-medium {{ background: #f39c12; }}
.risk-low {{ background: #27ae60; }}
.summary-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}}
.card {{
background: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}}
.card h3 {{
font-size: 14px;
color: #666;
text-transform: uppercase;
margin-bottom: 10px;
}}
.card .number {{
font-size: 36px;
font-weight: bold;
}}
.card .number.error {{ color: #e74c3c; }}
.card .number.warning {{ color: #f39c12; }}
.card .number.info {{ color: #3498db; }}
.section {{
background: white;
border-radius: 10px;
padding: 30px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}}
.section h2 {{
font-size: 20px;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #eee;
}}
table {{
width: 100%;
border-collapse: collapse;
font-size: 14px;
}}
th, td {{
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eee;
}}
th {{
background: #f8f9fa;
font-weight: 600;
color: #555;
}}
tr:hover {{ background: #f8f9fa; }}
.badge {{
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
}}
.badge-error {{ background: #fde8e8; color: #e74c3c; }}
.badge-warning {{ background: #fef3e2; color: #e67e22; }}
.badge-info {{ background: #e8f4fd; color: #3498db; }}
.badge-high {{ background: #fde8e8; color: #e74c3c; }}
.badge-medium {{ background: #fef3e2; color: #e67e22; }}
.badge-low {{ background: #e8f8e8; color: #27ae60; }}
code {{
background: #f4f4f4;
padding: 2px 6px;
border-radius: 3px;
font-size: 13px;
}}
.footer {{
text-align: center;
padding: 20px;
color: #999;
font-size: 13px;
}}
</style>
</head>
<body>
<div class="container">
<!– 报告头部 –>
<div class="header">
<h1>🔒 安全扫描报告</h1>
<div class="meta">
<p>项目:{self.project_name}</p>
<p>扫描时间:{summary['scan_date']}</p>
<p>扫描工具:Semgrep + OWASP ZAP + npm audit + Cursor AI</p>
</div>
<div class="risk-score {'risk-critical' if summary['risk_score'] >= 70 else 'risk-high' if summary['risk_score'] >= 50 else 'risk-medium' if summary['risk_score'] >= 25 else 'risk-low'}">
风险评分:{summary['risk_score']}/100
</div>
</div>
<!– 统计摘要 –>
<div class="summary-grid">
<div class="card">
<h3>SAST 静态扫描</h3>
<div class="number {'error' if summary['sast_by_severity']['ERROR'] > 0 else ''}">{summary['sast_total']}</div>
<p>🔴 ERROR: {summary['sast_by_severity']['ERROR']} | 🟡 WARNING: {summary['sast_by_severity']['WARNING']} | 🔵 INFO: {summary['sast_by_severity']['INFO']}</p>
</div>
<div class="card">
<h3>DAST 动态扫描</h3>
<div class="number">{summary['dast_total']}</div>
<p>🔴 High: {summary['dast_by_risk']['High']} | 🟠 Medium: {summary['dast_by_risk']['Medium']} | 🟡 Low: {summary['dast_by_risk']['Low']}</p>
</div>
<div class="card">
<h3>依赖漏洞</h3>
<div class="number">{summary['dependency_total']}</div>
<p>🔴 Critical: {summary['dependency_by_severity']['critical']} | 🟠 High: {summary['dependency_by_severity']['high']} | 🟡 Medium: {summary['dependency_by_severity']['medium']}</p>
</div>
<div class="card">
<h3>总发现数</h3>
<div class="number">{summary['total_issues']}</div>
<p>综合安全评估</p>
</div>
</div>
<!– 高危发现详情 –>
<div class="section">
<h2>🚨 高危发现(需立即处理)</h2>
{'<p style="color: #27ae60; font-size: 16px;">✅ 未发现高危安全问题</p>' if not critical_findings else ''}
{'<table><tr><th>类型</th><th>问题</th><th>位置</th><th>CWE</th></tr>' + ''.join(f'<tr><td><span class="badge badge-error">{f["type"]}</span></td><td>{f["title"]}</td><td><code>{f["location"]}</code></td><td>{f["cwe"]}</td></tr>' for f in critical_findings) + '</table>' if critical_findings else ''}
</div>
<!– SAST 详细结果 –>
<div class="section">
<h2>📋 SAST 静态分析详细结果</h2>
<table>
<tr>
<th>严重等级</th>
<th>规则</th>
<th>文件位置</th>
<th>描述</th>
<th>CWE</th>
</tr>
{''.join(f'<tr><td><span class="badge badge-{f["severity"].lower()}">{f["severity"]}</span></td><td>{f["rule_id"]}</td><td><code>{f["file"]}:{f["line"]}</code></td><td>{f["message"][:100]}</td><td>{f["cwe"]}</td></tr>' for f in self.data['sast_findings'][:50])}
</table>
</div>
<!– DAST 详细结果 –>
<div class="section">
<h2>🌐 DAST 动态分析详细结果</h2>
<table>
<tr>
<th>风险</th>
<th>漏洞名称</th>
<th>URL</th>
<th>修复建议</th>
</tr>
{''.join(f'<tr><td><span class="badge badge-{f["risk"].lower()}">{f["risk"]}</span></td><td>{f["name"]}</td><td><code>{f["url"][:60]}</code></td><td>{f["solution"][:100]}</td></tr>' for f in self.data['dast_findings'][:30])}
</table>
</div>
<!– 页脚 –>
<div class="footer">
<p>本报告由 Cursor 安全插件链自动生成 | 生成时间:{self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}</p>
<p>工具链:Semgrep + OWASP ZAP + npm audit + Cursor AI</p>
</div>
</div>
</body>
</html>"""
# 写入文件
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f" ✅ HTML 报告已生成: {output_path}")
def generate_markdown_report(self, output_path: str) –> None:
"""
生成 Markdown 格式的安全报告(适合在 Git 仓库中查看)
Args:
output_path: 输出文件路径
"""
summary = self.data['summary']
md_lines = [
f"# 🔒 安全扫描报告 – {self.project_name}",
f"",
f"**扫描时间**:{summary['scan_date']}",
f"**风险评分**:{summary['risk_score']}/100",
f"**总发现数**:{summary['total_issues']}",
f"",
f"—",
f"",
f"## 📊 统计摘要",
f"",
f"| 类别 | 总数 | 高危 | 中危 | 低危 |",
f"|——|——|——|——|——|",
f"| SAST | {summary['sast_total']} | {summary['sast_by_severity']['ERROR']} | {summary['sast_by_severity']['WARNING']} | {summary['sast_by_severity']['INFO']} |",
f"| DAST | {summary['dast_total']} | {summary['dast_by_risk']['High']} | {summary['dast_by_risk']['Medium']} | {summary['dast_by_risk']['Low']} |",
f"| 依赖 | {summary['dependency_total']} | {summary['dependency_by_severity']['critical'] + summary['dependency_by_severity']['high']} | {summary['dependency_by_severity']['medium']} | {summary['dependency_by_severity']['low']} |",
f"",
f"—",
f"",
f"## 🚨 SAST 发现详情",
f"",
]
# SAST 详情
for i, f in enumerate(self.data['sast_findings'], 1):
md_lines.extend([
f"### {i}. [{f['severity']}] {f['rule_id']}",
f"",
f"- **位置**:`{f['file']}:{f['line']}`",
f"- **CWE**:{f['cwe']}",
f"- **描述**:{f['message'][:200]}",
])
if f['code_snippet']:
md_lines.extend([
f"- **代码片段**:",
f"```",
f"{f['code_snippet'][:300]}",
f"```",
])
md_lines.append("")
# DAST 详情
md_lines.extend([
f"—",
f"",
f"## 🌐 DAST 发现详情",
f"",
])
for i, f in enumerate(self.data['dast_findings'], 1):
md_lines.extend([
f"### {i}. [{f['risk']}] {f['name']}",
f"",
f"- **URL**:`{f['url']}`",
f"- **CWE**:CWE-{f['cwe_id']}",
f"- **描述**:{f['description'][:200]}",
f"- **修复建议**:{f['solution'][:200]}",
f"",
])
# 写入文件
Path(output_path).parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w', encoding='utf-8') as f:
f.write('\\n'.join(md_lines))
print(f" ✅ Markdown 报告已生成: {output_path}")
def run(self, output_format: str = 'html') –> None:
"""
执行完整的报告生成流程
Args:
output_format: 输出格式 (html/markdown/json)
"""
print("╔══════════════════════════════════════════════╗")
print("║ 安全扫描报告生成器 v2.0 ║")
print("╚══════════════════════════════════════════════╝")
print(f" 项目: {self.project_name}")
print(f" 时间: {self.timestamp.strftime('%Y-%m-%d %H:%M:%S')}")
print(f" 格式: {output_format}")
print("")
# 步骤1:加载各类扫描结果
print("[1/3] 加载扫描数据…")
self.load_semgrep_results('./reports/semgrep-results.json')
self.load_zap_results('./reports/dast/zap-alerts-latest.json')
self.load_npm_audit('./reports/npm-audit.json')
# 步骤2:生成统计摘要
print("\\n[2/3] 生成统计摘要…")
self.generate_summary()
print(f" 风险评分: {self.data['summary']['risk_score']}/100")
# 步骤3:输出报告
print("\\n[3/3] 生成报告文件…")
timestamp_str = self.timestamp.strftime('%Y%m%d_%H%M%S')
if output_format == 'html':
self.generate_html_report(f'./reports/security-report-{timestamp_str}.html')
elif output_format == 'markdown':
self.generate_markdown_report(f'./reports/security-report-{timestamp_str}.md')
elif output_format == 'json':
json_path = f'./reports/security-report-{timestamp_str}.json'
with open(json_path, 'w', encoding='utf-8') as f:
json.dump(self.data, f, ensure_ascii=False, indent=2)
print(f" ✅ JSON 报告已生成: {json_path}")
print("\\n✅ 报告生成完成!")
# ==================== 命令行入口 ====================
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='安全扫描报告生成器')
parser.add_argument('–project', default='MyProject', help='项目名称')
parser.add_argument('–format', choices=['html', 'markdown', 'json'],
default='html', help='输出格式')
args = parser.parse_args()
generator = SecurityReportGenerator(args.project)
generator.run(output_format=args.format)
在 Cursor 中配置为 Task:
// .vscode/tasks.json 追加
{
"label": "生成安全报告 (HTML)",
"type": "shell",
"command": "python3",
"args": [
"scripts/generate_security_report.py",
"–project", "${workspaceFolderBasename}",
"–format", "html"
],
"group": "test",
"presentation": {
"reveal": "always",
"panel": "dedicated"
}
}
10.4 与漏洞管理平台对接
将扫描结果自动同步到企业漏洞管理平台(如 Jira、禅道、自研平台):
#!/usr/bin/env python3
"""
sync_to_tracker.py – 将安全发现同步到漏洞管理平台
支持平台:
– Jira (Atlassian)
– 禅道 (ZenTao)
– 自定义 Webhook
用法: python3 scripts/sync_to_tracker.py –platform jira –config tracker-config.yaml
"""
import json
import yaml
import requests
from datetime import datetime
from typing import List, Dict, Optional
from dataclasses import dataclass, asdict
@dataclass
class VulnerabilityTicket:
"""漏洞工单数据结构"""
title: str # 标题
description: str # 详细描述
severity: str # 严重等级: Critical/High/Medium/Low
vuln_type: str # 漏洞类型: SAST/DAST/Dependency
cwe_id: str # CWE 编号
affected_file: str # 影响文件
affected_line: int # 影响行号
fix_suggestion: str # 修复建议
assignee: str # 指派给
labels: List[str] # 标签
due_date: str # 截止日期
source_tool: str # 来源工具
class JiraSyncer:
"""Jira 漏洞同步器"""
def __init__(self, config: Dict):
"""
初始化 Jira 同步器
Args:
config: Jira 连接配置
– base_url: Jira 服务器地址
– project_key: 项目标识
– username: 用户名
– api_token: API Token
– issue_type: 工单类型(默认 Bug)
"""
self.base_url = config['base_url'].rstrip('/')
self.project_key = config['project_key']
self.username = config['username']
self.api_token = config['api_token']
self.issue_type = config.get('issue_type', 'Bug')
self.session = requests.Session()
self.session.auth = (self.username, self.api_token)
self.session.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
def create_ticket(self, vuln: VulnerabilityTicket) –> Optional[str]:
"""
在 Jira 中创建漏洞工单
Args:
vuln: 漏洞信息
Returns:
创建的工单 Key(如 SEC-123),失败返回 None
"""
# 严重等级映射到 Jira Priority
priority_map = {
'Critical': 'Highest',
'High': 'High',
'Medium': 'Medium',
'Low': 'Low'
}
# 构建 Jira Issue 数据
issue_data = {
"fields": {
"project": {"key": self.project_key},
"summary": f"[{vuln.vuln_type}][{vuln.severity}] {vuln.title}",
"description": {
"type": "doc",
"version": 1,
"content": [
{
"type": "paragraph",
"content": [{"type": "text", "text": vuln.description}]
},
{
"type": "heading",
"attrs": {"level": 3},
"content": [{"type": "text", "text": "修复建议"}]
},
{
"type": "paragraph",
"content": [{"type": "text", "text": vuln.fix_suggestion}]
},
{
"type": "heading",
"attrs": {"level": 3},
"content": [{"type": "text", "text": "影响位置"}]
},
{
"type": "codeBlock",
"content": [{"type": "text", "text": f"{vuln.affected_file}:{vuln.affected_line}"}]
}
]
},
"issuetype": {"name": self.issue_type},
"priority": {"name": priority_map.get(vuln.severity, 'Medium')},
"labels": vuln.labels + [
f"cwe-{vuln.cwe_id}",
vuln.vuln_type.lower(),
"security"
],
"customfield_10001": vuln.cwe_id, # 自定义字段:CWE
"customfield_10002": vuln.source_tool # 自定义字段:来源工具
}
}
try:
response = self.session.post(
f"{self.base_url}/rest/api/3/issue",
json=issue_data
)
if response.status_code == 201:
issue_key = response.json()['key']
print(f" ✅ 工单创建成功: {issue_key}")
return issue_key
else:
print(f" ❌ 工单创建失败: {response.status_code} – {response.text}")
return None
except requests.RequestException as e:
print(f" ❌ 网络错误: {e}")
return None
def batch_sync(self, findings: List[Dict], source: str) –> Dict[str, int]:
"""
批量同步扫描发现到 Jira
Args:
findings: 发现列表
source: 来源(SAST/DAST/Dependency)
Returns:
同步统计 {'success': n, 'failed': n, 'skipped': n}
"""
stats = {'success': 0, 'failed': 0, 'skipped': 0}
for finding in findings:
# 跳过 INFO 级别
severity = finding.get('severity', finding.get('risk', 'INFO'))
if severity in ('INFO', 'Informational'):
stats['skipped'] += 1
continue
# 检查是否已存在相同工单(避免重复)
if self._check_duplicate(finding):
stats['skipped'] += 1
continue
# 构建工单
ticket = VulnerabilityTicket(
title=finding.get('rule_id', finding.get('name', 'Unknown')),
description=finding.get('message', finding.get('description', '')),
severity=self._map_severity(severity),
vuln_type=source,
cwe_id=finding.get('cwe', finding.get('cweid', 'N/A')),
affected_file=finding.get('file', finding.get('url', '')),
affected_line=finding.get('line', 0),
fix_suggestion=finding.get('fix_suggestion', finding.get('solution', '')),
assignee='security-team',
labels=['auto-generated', source.lower()],
due_date=self._calculate_due_date(severity),
source_tool=finding.get('tool', 'Semgrep')
)
result = self.create_ticket(ticket)
if result:
stats['success'] += 1
else:
stats['failed'] += 1
return stats
def _check_duplicate(self, finding: Dict) –> bool:
"""检查是否已存在相同的漏洞工单"""
rule_id = finding.get('rule_id', finding.get('name', ''))
file_path = finding.get('file', finding.get('url', ''))
# 使用 JQL 搜索
jql = f'project = {self.project_key} AND labels = "security" AND summary ~ "{rule_id}" AND summary ~ "{file_path}"'
try:
response = self.session.get(
f"{self.base_url}/rest/api/3/search",
params={'jql': jql, 'maxResults': 1}
)
if response.status_code == 200:
return response.json().get('total', 0) > 0
except requests.RequestException:
pass
return False
def _map_severity(self, severity: str) –> str:
"""映射严重等级"""
mapping = {
'ERROR': 'Critical',
'WARNING': 'Medium',
'High': 'High',
'Medium': 'Medium',
'Low': 'Low',
'critical': 'Critical',
'high': 'High',
'medium': 'Medium',
'low': 'Low'
}
return mapping.get(severity, 'Medium')
def _calculate_due_date(self, severity: str) –> str:
"""根据严重等级计算截止日期"""
from datetime import timedelta
days_map = {
'Critical': 1, # 1天内修复
'High': 3, # 3天内修复
'Medium': 7, # 7天内修复
'Low': 30 # 30天内修复
}
days = days_map.get(severity, 7)
due = datetime.now() + timedelta(days=days)
return due.strftime('%Y-%m-%d')
class WebhookSyncer:
"""通用 Webhook 同步器(适用于自研平台)"""
def __init__(self, webhook_url: str, headers: Optional[Dict] = None):
self.webhook_url = webhook_url
self.headers = headers or {'Content-Type': 'application/json'}
def send(self, findings: List[Dict], metadata: Dict) –> bool:
"""发送发现数据到 Webhook"""
payload = {
'timestamp': datetime.now().isoformat(),
'metadata': metadata,
'findings': findings,
'total_count': len(findings)
}
try:
response = requests.post(
self.webhook_url,
json=payload,
headers=self.headers,
timeout=30
)
return response.status_code in (200, 201, 202)
except requests.RequestException as e:
print(f" ❌ Webhook 发送失败: {e}")
return False
# ==================== 主执行逻辑 ====================
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='漏洞管理平台同步工具')
parser.add_argument('–platform', choices=['jira', 'zentao', 'webhook'],
default='webhook', help='目标平台')
parser.add_argument('–config', default='./tracker-config.yaml',
help='配置文件路径')
args = parser.parse_args()
print("╔══════════════════════════════════════════╗")
print("║ 漏洞管理平台同步工具 ║")
print("╚══════════════════════════════════════════╝")
# 加载配置
if os.path.exists(args.config):
with open(args.config, 'r') as f:
config = yaml.safe_load(f)
else:
print(f"⚠️ 配置文件不存在: {args.config}")
print(" 使用默认 Webhook 模式")
config = {'webhook_url': 'http://localhost:9000/api/security/findings'}
# 加载扫描结果
findings = []
if os.path.exists('./reports/semgrep-results.json'):
with open('./reports/semgrep-results.json') as f:
data = json.load(f)
findings.extend(data.get('results', []))
print(f"\\n 待同步发现: {len(findings)} 条")
# 执行同步
if args.platform == 'jira':
syncer = JiraSyncer(config.get('jira', {}))
stats = syncer.batch_sync(findings, 'SAST')
print(f"\\n 同步完成: 成功 {stats['success']}, 失败 {stats['failed']}, 跳过 {stats['skipped']}")
elif args.platform == 'webhook':
syncer = WebhookSyncer(config.get('webhook_url', ''))
success = syncer.send(findings, {'project': 'secure-blog-api', 'scan_type': 'SAST'})
print(f"\\n {'✅ 同步成功' if success else '❌ 同步失败'}")
配置文件示例 tracker-config.yaml:
# tracker-config.yaml – 漏洞管理平台对接配置
# Jira 配置
jira:
base_url: "https://your-company.atlassian.net"
project_key: "SEC"
username: "security-bot@company.com"
api_token: "${JIRA_API_TOKEN}" # 使用环境变量
issue_type: "Bug"
# 严重等级到 Jira Priority 的映射
priority_mapping:
Critical: "Highest"
High: "High"
Medium: "Medium"
Low: "Low"
# Webhook 配置(自研平台)
webhook_url: "https://internal-api.company.com/api/v2/security/findings"
webhook_headers:
Authorization: "Bearer ${INTERNAL_API_TOKEN}"
X-Source: "cursor-security-chain"
# 同步策略
sync_policy:
# 最低同步等级(低于此等级不同步)
min_severity: "Medium"
# 是否自动指派
auto_assign: true
# 默认指派人
default_assignee: "security-team"
# 去重策略
dedup_strategy: "rule_id + file_path"
# 同步频率
sync_interval: "on_scan_complete" # on_scan_complete / hourly / daily
10.5 自定义仪表盘与度量
创建团队安全度量仪表盘配置:
#!/usr/bin/env python3
"""
security_metrics.py – 安全度量数据收集与仪表盘数据生成
追踪团队安全趋势,为管理决策提供数据支撑
"""
import json
import sqlite3
from datetime import datetime, timedelta
from pathlib import Path
from typing import Dict, List
class SecurityMetricsDB:
"""安全度量数据库管理"""
def __init__(self, db_path: str = './reports/metrics/security_metrics.db'):
"""初始化 SQLite 数据库"""
Path(db_path).parent.mkdir(parents=True, exist_ok=True)
self.conn = sqlite3.connect(db_path)
self._init_tables()
def _init_tables(self):
"""初始化数据表"""
cursor = self.conn.cursor()
# 扫描记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS scan_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scan_date TEXT NOT NULL,
project TEXT NOT NULL,
scan_type TEXT NOT NULL, — SAST/DAST/Dependency
total_findings INTEGER DEFAULT 0,
critical_count INTEGER DEFAULT 0,
high_count INTEGER DEFAULT 0,
medium_count INTEGER DEFAULT 0,
low_count INTEGER DEFAULT 0,
info_count INTEGER DEFAULT 0,
risk_score INTEGER DEFAULT 0,
scan_duration_seconds REAL DEFAULT 0,
tool_version TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
# 漏洞修复记录表
cursor.execute('''
CREATE TABLE IF NOT EXISTS fix_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
finding_id TEXT NOT NULL,
rule_id TEXT NOT NULL,
severity TEXT NOT NULL,
file_path TEXT,
detected_at TEXT NOT NULL,
fixed_at TEXT,
fix_duration_hours REAL,
fixed_by TEXT,
fix_commit TEXT,
status TEXT DEFAULT 'open' — open/fixed/false_positive/wont_fix
)
''')
# 团队安全指标表
cursor.execute('''
CREATE TABLE IF NOT EXISTS team_metrics (
id INTEGER PRIMARY KEY AUTOINCREMENT,
period TEXT NOT NULL, — 统计周期 (weekly/monthly)
period_start TEXT NOT NULL,
period_end TEXT NOT NULL,
team_name TEXT NOT NULL,
total_scans INTEGER DEFAULT 0,
total_findings INTEGER DEFAULT 0,
findings_fixed INTEGER DEFAULT 0,
avg_fix_time_hours REAL DEFAULT 0,
false_positive_rate REAL DEFAULT 0,
code_coverage_percent REAL DEFAULT 0,
security_training_completed INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
)
''')
self.conn.commit()
def record_scan(self, scan_data: Dict) –> None:
"""记录一次扫描结果"""
cursor = self.conn.cursor()
cursor.execute('''
INSERT INTO scan_records
(scan_date, project, scan_type, total_findings,
critical_count, high_count, medium_count, low_count,
info_count, risk_score, scan_duration_seconds, tool_version)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
scan_data['scan_date'],
scan_data['project'],
scan_data['scan_type'],
scan_data['total_findings'],
scan_data.get('critical_count', 0),
scan_data.get('high_count', 0),
scan_data.get('medium_count', 0),
scan_data.get('low_count', 0),
scan_data.get('info_count', 0),
scan_data.get('risk_score', 0),
scan_data.get('duration', 0),
scan_data.get('tool_version', '')
))
self.conn.commit()
def get_trend_data(self, project: str, days: int = 30) –> List[Dict]:
"""获取趋势数据(用于仪表盘图表)"""
cursor = self.conn.cursor()
since_date = (datetime.now() – timedelta(days=days)).isoformat()
cursor.execute('''
SELECT scan_date, scan_type, total_findings,
critical_count, high_count, risk_score
FROM scan_records
WHERE project = ? AND scan_date >= ?
ORDER BY scan_date ASC
''', (project, since_date))
columns = ['date', 'type', 'total', 'critical', 'high', 'risk_score']
return [dict(zip(columns, row)) for row in cursor.fetchall()]
def generate_dashboard_data(self, project: str) –> Dict:
"""生成仪表盘数据(JSON 格式,可供前端渲染)"""
trend = self.get_trend_data(project, days=30)
# 计算关键指标
total_scans = len(trend)
avg_findings = sum(t['total'] for t in trend) / max(total_scans, 1)
latest_risk = trend[–1]['risk_score'] if trend else 0
# 趋势判断(与7天前对比)
week_ago = [t for t in trend if t['date'] >= (datetime.now() – timedelta(days=7)).isoformat()]
prev_week = [t for t in trend if t['date'] < (datetime.now() – timedelta(days=7)).isoformat()]
trend_direction = 'stable'
if week_ago and prev_week:
recent_avg = sum(t['total'] for t in week_ago) / len(week_ago)
prev_avg = sum(t['total'] for t in prev_week) / max(len(prev_week), 1)
if recent_avg > prev_avg * 1.2:
trend_direction = 'worsening'
elif recent_avg < prev_avg * 0.8:
trend_direction = 'improving'
return {
'project': project,
'generated_at': datetime.now().isoformat(),
'summary': {
'total_scans_30d': total_scans,
'avg_findings_per_scan': round(avg_findings, 1),
'latest_risk_score': latest_risk,
'trend_direction': trend_direction
},
'trend_data': trend,
'recommendations': self._generate_recommendations(trend)
}
def _generate_recommendations(self, trend: List[Dict]) –> List[str]:
"""基于趋势数据生成建议"""
recommendations = []
if not trend:
return ["暂无数据,请先运行安全扫描"]
latest = trend[–1]
if latest['critical'] > 0:
recommendations.append(f"⚠️ 当前有 {latest['critical']} 个 Critical 级别问题需要立即处理")
if latest['risk_score'] > 70:
recommendations.append("🔴 风险评分过高,建议暂停新功能开发,集中修复安全问题")
elif latest['risk_score'] > 50:
recommendations.append("🟠 风险评分偏高,建议本迭代优先处理安全问题")
# 检查趋势
if len(trend) >= 7:
recent_7 = trend[–7:]
if all(t['total'] > 10 for t in recent_7):
recommendations.append("📈 连续7天发现数超过10,建议加强安全编码培训")
if not recommendations:
recommendations.append("✅ 安全状况良好,请继续保持当前实践")
return recommendations
def close(self):
"""关闭数据库连接"""
self.conn.close()
# ==================== 使用示例 ====================
if __name__ == '__main__':
print("╔══════════════════════════════════════════╗")
print("║ 安全度量数据收集 ║")
print("╚══════════════════════════════════════════╝")
db = SecurityMetricsDB()
# 记录本次扫描
scan_record = {
'scan_date': datetime.now().isoformat(),
'project': 'secure-blog-api',
'scan_type': 'SAST',
'total_findings': 8,
'critical_count': 2,
'high_count': 3,
'medium_count': 2,
'low_count': 1,
'info_count': 0,
'risk_score': 65,
'duration': 12.5,
'tool_version': 'semgrep 1.70.0'
}
db.record_scan(scan_record)
print(f" ✅ 扫描记录已保存")
# 生成仪表盘数据
dashboard = db.generate_dashboard_data('secure-blog-api')
# 输出仪表盘 JSON
output_path = './reports/metrics/dashboard-data.json'
with open(output_path, 'w', encoding='utf-8') as f:
json.dump(dashboard, f, ensure_ascii=False, indent=2)
print(f" ✅ 仪表盘数据已生成: {output_path}")
print(f"\\n 📊 风险评分: {dashboard['summary']['latest_risk_score']}/100")
print(f" 📈 趋势: {dashboard['summary']['trend_direction']}")
for rec in dashboard['recommendations']:
print(f" {rec}")
db.close()
常见陷阱与问题排除
本节汇总在使用 Cursor 安全插件链过程中最常遇到的陷阱和错误,提供快速解决方案。
陷阱一:Semgrep 规则匹配不到预期代码
现象:规则语法正确,但扫描时不报告预期的漏洞。
原因:
- 模式匹配过于严格(缩进、空格、换行不一致)
- 语言标识不正确
- 被 pattern-not 意外排除
解决方案:
# 1. 使用 –debug 查看详细匹配过程
semgrep –config my-rule.yaml –debug target-file.js 2>&1 | grep -A5 "matching"
# 2. 使用 Semgrep Playground 在线调试
# 访问 https://semgrep.dev/playground
# 粘贴代码和规则,实时查看匹配结果
# 3. 简化规则逐步添加条件
# 先测试最基础的 pattern,确认能匹配
# 再逐步添加 pattern-not、metavariable-regex 等条件
# 4. 检查语言标识
# JavaScript 文件必须用: languages: [javascript]
# TypeScript 文件必须用: languages: [typescript]
# 两者通用: languages: [javascript, typescript]
陷阱二:Cursor 扩展与 CLI 版本不一致
现象:CLI 扫描正常,但 Cursor 中无提示或报错。
解决方案:
# 检查 CLI 版本
semgrep –version
# 检查扩展版本
# Cursor → Extensions → 搜索 Semgrep → 查看版本号
# 确保兼容:扩展版本应 >= CLI 版本
# 如果不一致:
pip3 install –upgrade semgrep
# 然后重启 Cursor(Ctrl+Shift+P → "Reload Window")
陷阱三:大型 monorepo 扫描极慢
现象:扫描耗时超过 10 分钟,Cursor 卡顿。
解决方案:
# 1. 精确配置 .semgrepignore
# 排除所有非源码目录
# 2. 使用 –include 只扫描目标语言
semgrep –config auto –include="*.ts" –include="*.tsx" ./packages/
# 3. 分模块扫描(在 tasks.json 中配置多个任务)
# 4. 关闭实时扫描,仅保存时扫描
# 5. 增加 –jobs 并行度
semgrep –config auto –jobs 8 ./src/
陷阱四:Docker 中 ZAP 无法访问 localhost
现象:ZAP 容器报告目标不可达。
原因:Docker 容器中的 localhost 指向容器自身,而非宿主机。
解决方案:
# 方案1:使用 host 网络模式
docker run –network="host" ghcr.io/zaproxy/zaproxy:stable ...
# 方案2:使用宿主机 IP
# macOS/Windows: host.docker.internal
# Linux: 172.17.0.1 或 –add-host=host.docker.internal:host-gateway
docker run –add-host=host.docker.internal:host-gateway \\
ghcr.io/zaproxy/zaproxy:stable \\
zap-baseline.py -t http://host.docker.internal:3000
# 方案3:将应用也放入 Docker 网络
docker network create security-test-net
docker run –network security-test-net –name myapp myapp:latest
docker run –network security-test-net ghcr.io/zaproxy/zaproxy:stable \\
zap-baseline.py -t http://myapp:3000
陷阱五:AI 提示词产生幻觉(误报不存在的漏洞)
现象:Cursor AI 报告了代码中实际不存在的安全问题。
解决方案:
# 在提示词中添加约束:
"请仅基于我提供的代码进行分析,不要假设代码中存在未展示的逻辑。
如果无法确定某个问题是否真实存在,请标注为'需要人工确认'。
不要编造代码中不存在的函数调用或变量。"
# 同时要求 AI 给出证据:
"对每个发现,请引用具体的代码行作为证据。"
陷阱六:Git Hooks 导致无法正常提交
现象:pre-commit hook 中的安全扫描阻止了紧急修复提交。
解决方案:
# 紧急情况下跳过 hook(不推荐常规使用)
git commit –no-verify -m "hotfix: urgent security patch"
# 更好的方案:在 hook 中添加超时和降级机制
# 在 pre-commit 脚本中:
timeout 60 semgrep –config auto ./src || echo "⚠️ 扫描超时,允许提交"
# 或者配置为仅警告不阻塞:
semgrep –config auto ./src –severity=ERROR –error || true
陷阱七:环境变量未正确加载
现象:process.env.JWT_SECRET_KEY 为 undefined。
解决方案:
# 1. 确认 .env 文件存在且格式正确
# .env
JWT_SECRET_KEY=your-256-bit-secret-key-here
DB_HOST=localhost
DB_PASSWORD=your-password
# 2. 确认 .env 在 .gitignore 中
echo ".env" >> .gitignore
# 3. 在应用入口加载
# app.js 顶部
require('dotenv').config();
# 4. Cursor 终端中确认
echo $JWT_SECRET_KEY # 应输出值
# 5. 如果使用 Docker,通过 –env-file 传入
docker run –env-file .env myapp:latest
总结
本教程从"安全左移"的核心理念出发,系统性地介绍了如何在 Cursor IDE 中构建一套完整的 SAST/DAST 辅助分析工作流。以下是各章节的核心要点回顾:
核心收获
| 一 | 理解安全左移理念 | – |
| 二 | 环境搭建与插件安装 | Semgrep, SonarLint, Bandit, ZAP |
| 三 | 自定义 SAST 规则 | Semgrep YAML 规则语法 |
| 四 | DAST 动态测试 | OWASP ZAP, 自定义扫描脚本 |
| 五 | AI 安全提示词工程 | Cursor AI, .cursorrules |
| 六 | 漏洞修复闭环 | 参数化查询, bcrypt, DOMPurify |
| 七 | 误报优化 | taint 模式, 白名单, 上下文过滤 |
| 八 | 团队协作标准化 | Git Hooks, Review 清单, 培训计划 |
| 九 | 故障排查 | 版本管理, 网络配置, 性能优化 |
| 十 | CI/CD 与进阶 | GitHub Actions, Jira 对接, 度量仪表盘 |
实施路线图建议
第 1 周(基础搭建):
- 完成环境搭建(第二章)
- 安装核心插件并验证基本功能
- 编写第一批基础安全规则
第 2-3 周(规则完善):
- 根据项目技术栈定制规则(第三章)
- 配置 AI 安全审查提示词(第五章)
- 完成第一次完整的安全扫描
第 4 周(流程集成):
- 配置 Git Hooks(第八章)
- 建立 Code Review 安全检查清单
- 完成第一次团队安全培训
第 2 个月(优化迭代):
- 根据误报反馈优化规则(第七章)
- 集成 CI/CD 管道(第十章)
- 建立安全度量体系
第 3 个月及以后(持续改进):
- 扩展私有规则库
- 对接漏洞管理平台
- 定期安全培训与演练
关键原则
详细参考资料
官方文档
| Semgrep 官方文档 | https://semgrep.dev/docs/ | 规则编写、CLI 使用 |
| Semgrep 规则注册表 | https://semgrep.dev/explore/ | 社区规则库 |
| OWASP ZAP 文档 | https://www.zaproxy.org/docs/ | DAST 工具使用 |
| OWASP Top 10 | https://owasp.org/Top10/ | 十大 Web 安全风险 |
| Cursor 官方文档 | https://docs.cursor.com/ | IDE 使用指南 |
| Bandit 文档 | https://bandit.readthedocs.io/ | Python 安全扫描 |
| ESLint Security | https://github.com/nodesecurity/eslint-plugin-security | JS 安全插件 |
| SonarLint 文档 | https://docs.sonarsource.com/sonarlint/ | 代码质量 |
CWE/OWASP 参考
| CWE-78 | OS 命令注入 | 3.3, 6.2 |
| CWE-79 | 跨站脚本 (XSS) | 3.3, 5.3 |
| CWE-89 | SQL 注入 | 3.2, 5.2, 6.2 |
| CWE-22 | 路径遍历 | 4.4 |
| CWE-330 | 不安全随机数 | 3.3.3 |
| CWE-502 | 不安全反序列化 | 3.3.2 |
| CWE-532 | 日志信息泄露 | 3.2.1, 5.5 |
| CWE-639 | 越权访问 (IDOR) | 5.4, 6.2 |
| CWE-798 | 硬编码凭证 | 3.3.2, 5.5 |
推荐书籍
- 《Web 应用安全权威指南》- 德丸浩
- 《白帽子讲 Web 安全》- 吴翰清
- 《Secure Coding in C and C++》- Robert C. Seacord
- 《The Tangled Web》- Michal Zalewski
- 《OWASP Testing Guide》- OWASP Foundation
社区与学习资源
- OWASP 本地分会:参加线下安全交流活动
- Semgrep Slack:https://semgrep.dev/slack(规则编写交流)
- CTF 练习平台:DVWA、WebGoat、HackTheBox
- GitHub 安全规则库:
- https://github.com/semgrep/semgrep-rules
- https://github.com/trailofbits/semgrep-rules
- https://github.com/returntocorp/semgrep-rules
附录
附录 A:完整规则文件模板
# semgrep-rules/TEMPLATE.yaml
# Semgrep 规则模板 – 复制此文件创建新规则
# 文档: https://semgrep.dev/docs/writing-rules/rule-syntax/
rules:
# ============================================
# 规则模板 1:简单模式匹配
# ============================================
– id: template–simple–pattern
# 匹配模式:使用目标语言的实际语法
pattern: |
dangerous_function($ARG)
# 检测到时的提示信息(支持多行)
message: >
检测到使用 dangerous_function()。
此函数存在安全风险,请使用 safe_alternative() 替代。
# 适用语言(可多选)
languages:
– javascript
– typescript
# 严重等级:INFO / WARNING / ERROR
severity: WARNING
# 元数据(用于分类和报告)
metadata:
category: security
cwe: "CWE-XXX"
owasp: "AXX:2021"
confidence: MEDIUM # LOW / MEDIUM / HIGH / VERY_HIGH
likelihood: MEDIUM
impact: MEDIUM
subcategory:
– audit # vuln(确定漏洞) / audit(需确认) / guardrail(防护)
references:
– https://owasp.org/Top10/
technology:
– nodejs
# ============================================
# 规则模板 2:多模式匹配(任一匹配即报警)
# ============================================
– id: template–multi–pattern
patterns:
– pattern-either:
– pattern: func_a($X)
– pattern: func_b($X)
– pattern: func_c($X)
# 排除条件
– pattern-not: func_a("safe_literal")
message: "检测到危险函数调用"
languages: [javascript]
severity: ERROR
# ============================================
# 规则模板 3:污点追踪模式(数据流分析)
# ============================================
– id: template–taint–tracking
mode: taint
# 不可信数据来源(污点源)
pattern-sources:
– pattern: req.query.$PARAM
– pattern: req.body.$PARAM
– pattern: req.params.$PARAM
– pattern: req.headers.$PARAM
– pattern: window.location.$PROP
# 危险操作(污点汇)
pattern-sinks:
– pattern: db.query($SQL)
– pattern: eval($CODE)
– pattern: document.write($HTML)
– pattern: $EL.innerHTML = $VAL
# 净化操作(经过处理的数据视为安全)
pattern-sanitizers:
– pattern: sanitize($X)
– pattern: escape($X)
– pattern: parseInt($X)
– pattern: DOMPurify.sanitize($X)
message: >
不可信数据从 $SOURCE 流入危险操作,
中间未经过有效的安全净化处理。
languages: [javascript, typescript]
severity: ERROR
metadata:
confidence: VERY_HIGH
# ============================================
# 规则模板 4:带自动修复的规则
# ============================================
– id: template–with–fix
pattern: |
md5($INPUT)
fix: |
bcrypt.hash($INPUT, 12)
message: >
MD5 不是安全的哈希算法,请使用 bcrypt。
已提供自动修复建议。
languages: [python]
severity: ERROR
metadata:
cwe: "CWE-328"
附录 B:提示词速查表
| 快速安全扫描 | “扫描这段代码的安全问题,按严重性排序列出” | Ctrl+L |
| SQL 注入检查 | “检查此代码是否存在 SQL 注入,关注字符串拼接和参数化” | Ctrl+L |
| XSS 检查 | “检查此代码的 XSS 风险,关注 innerHTML/v-html/模板输出” | Ctrl+L |
| 认证检查 | “审查认证逻辑:密码哈希、token 过期、暴力破解防护” | Ctrl+L |
| 修复建议 | “给出此漏洞的完整修复代码,包含输入验证和错误处理” | Ctrl+K |
| 代码审查 | “以安全工程师角色审查此 PR 代码,输出安全检查清单” | Ctrl+L |
| 解释漏洞 | “用通俗语言解释这个漏洞的攻击原理和影响” | Ctrl+L |
| 生成测试 | “为这个安全修复编写单元测试,验证漏洞已修复” | Ctrl+K |
| 规则编写 | “为以下漏洞模式编写 Semgrep 规则:[描述漏洞模式]” | Ctrl+L |
| 配置审查 | “检查此配置文件的安全问题:CORS/CSP/安全头/调试模式” | Ctrl+L |
提示词编写黄金公式:
[角色] + [任务] + [上下文/约束] + [输出格式] + [注意事项]
示例:
"你是一位资深安全工程师(角色),
请审查以下 Express.js 代码中的注入漏洞(任务),
重点关注 OWASP Top 10 中的 A03 注入类漏洞(约束),
以表格形式输出:漏洞类型|严重等级|位置|修复方案(格式),
只报告确定的问题,不确定的标注'需确认'(注意事项)。"
附录 C:故障排查决策树
安全插件链异常
│
├─ 插件无法安装
│ ├─ 网络错误 → 检查代理/镜像源
│ ├─ 权限不足 → sudo / 管理员权限
│ └─ 版本不兼容 → 检查 Node/Python 版本
│
├─ 插件已安装但不工作
│ ├─ 无输出/无提示
│ │ ├─ 检查 CLI 是否在 PATH 中 → which semgrep
│ │ ├─ 检查扩展设置路径是否正确
│ │ └─ 重启 Cursor (Reload Window)
│ ├─ 报错退出
│ │ ├─ 查看 Output 面板 (View → Output → 选择扩展)
│ │ ├─ 检查规则 YAML 语法
│ │ └─ 检查文件编码 (UTF-8)
│ └─ 扫描超时
│ ├─ 添加 .semgrepignore 排除大目录
│ ├─ 减少规则数量
│ └─ 增加 timeout 配置
│
├─ 扫描结果不准确
│ ├─ 误报太多
│ │ ├─ 添加 pattern-not 排除条件
│ │ ├─ 使用 taint 模式替代简单匹配
│ │ ├─ 配置 .semgrepignore
│ │ └─ 使用 nosemgrep 注释抑制
│ └─ 漏报
│ ├─ 检查规则是否覆盖目标语言
│ ├─ 添加更多 pattern-either 分支
│ ├─ 启用更多官方规则集
│ └─ 使用 Semgrep Playground 调试
│
├─ DAST 扫描失败
│ ├─ 目标不可达 → 确认应用已启动
│ ├─ Docker 网络问题 → 使用 –network host
│ ├─ 认证失败 → 配置 ZAP 认证脚本
│ └─ 超时 → 增加 timeout 参数
│
└─ CI/CD 集成失败
├─ 环境变量未设置 → 配置 Secrets
├─ 权限不足 → 检查 token 权限
├─ 依赖安装失败 → 使用缓存
└─ 报告上传失败 → 检查 artifact 配置
附录 D:推荐学习资源与进阶路径
入门阶段(1-2 周):
| OWASP Top 10 官方文档 | 阅读 | 4h |
| Semgrep 官方教程 | 互动 | 3h |
| DVWA 漏洞靶场练习 | 实操 | 8h |
| Cursor 官方文档 | 阅读 | 2h |
进阶阶段(3-4 周):
| Semgrep 规则编写高级教程 | 实操 | 6h |
| OWASP ZAP 完整指南 | 阅读+实操 | 8h |
| WebGoat 全关卡通关 | 实操 | 16h |
| 安全代码审查实践 | 实操 | 持续 |
高级阶段(1-3 月):
| 自定义 taint 分析规则 | 实操 | 持续 |
| CI/CD 安全管道设计 | 设计+实施 | 2周 |
| 企业安全度量体系 | 设计 | 1周 |
| 安全培训体系建设 | 规划 | 持续 |
| CTF 竞赛参与 | 实操 | 持续 |
认证推荐:
- CSSLP(Certified Secure Software Lifecycle Professional)
- CEH(Certified Ethical Hacker)
- CompTIA Security+
- OWASP 相关培训认证
附录 E:项目完整文件清单
以下是本教程涉及的完整项目文件结构,可作为实际项目的参考模板:
secure-blog-api/
│
├── .cursor/
│ └── rules/
│ └── security-review.mdc # Cursor AI 安全审查规则
│
├── .cursorrules # 项目级 AI 配置
├── .eslintrc.json # ESLint 安全规则配置
├── .semgrepignore # Semgrep 忽略文件
├── .env.example # 环境变量模板
├── .gitignore # Git 忽略(含 .env)
├── package.json # 项目依赖
├── SECURITY_GUIDELINES.md # 安全编码规范
├── tracker-config.yaml # 漏洞平台对接配置
│
├── .husky/
│ ├── pre-commit # 提交前安全检查
│ └── pre-push # 推送前完整扫描
│
├── .vscode/
│ ├── settings.json # Cursor/VS Code 设置
│ └── tasks.json # 自动化任务配置
│
├── src/
│ ├── app.js # 应用入口
│ ├── routes/ # 路由定义
│ ├── controllers/ # 控制器(含安全修复)
│ ├── middleware/
│ │ ├── auth.js # JWT 认证中间件
│ │ ├── validate.js # 输入验证中间件
│ │ ├── rateLimiter.js # 速率限制中间件
│ │ └── errorHandler.js # 安全错误处理
│ ├── models/ # 数据模型
│ └── utils/
│ ├── sanitize.js # 输入净化
│ ├── logger.js # 安全日志
│ └── crypto.js # 加密工具
│
├── semgrep-rules/ # 自定义 SAST 规则
│ ├── injection/
│ │ ├── sql-injection.yaml
│ │ ├── command-injection.yaml
│ │ └── test-sql-injection.js # 规则测试用例
│ ├── xss/
│ │ ├── reflected-xss.yaml
│ │ └── stored-xss.yaml
│ ├── auth/
│ │ ├── weak-crypto.yaml
│ │ ├── hardcoded-secrets.yaml
│ │ └── insecure-session.yaml
│ └── business/
│ ├── payment-security.yaml
│ └── pii-handling.yaml
│
├── scripts/
│ ├── dast-scan.sh # DAST 扫描脚本
│ ├── dast-workflow.sh # 完整 DAST 工作流
│ ├── api_security_scanner.py # API 安全扫描器
│ ├── parse_dast_report.py # DAST 报告解析
│ ├── generate_security_report.py # 综合报告生成
│ ├── sync_to_tracker.py # 漏洞平台同步
│ ├── security_metrics.py # 安全度量收集
│ └── feedback-loop.sh # 误报反馈收集
│
├── reports/ # 扫描报告输出目录
│ ├── semgrep-results.json
│ ├── npm-audit.json
│ ├── dast/
│ │ ├── zap-report-*.html
│ │ ├── zap-alerts-*.json
│ │ └── fix-suggestions.md
│ └── metrics/
│ ├── security_metrics.db
│ └── dashboard-data.json
│
├── tests/
│ ├── security/
│ │ ├── test-sql-injection.js # SQL 注入修复验证
│ │ ├── test-xss.js # XSS 修复验证
│ │ └── test-auth.js # 认证安全验证
│ └── integration/
│
└── .github/
└── workflows/
└── security-scan.yml # CI/CD 安全管道
附录 F:快速启动命令速查
# ===== 环境初始化 =====
pip3 install semgrep bandit # 安装 Python 安全工具
npm install -D eslint eslint-plugin-security # 安装 JS 安全插件
docker pull ghcr.io/zaproxy/zaproxy:stable # 拉取 ZAP 镜像
# ===== 日常扫描 =====
semgrep –config auto –config ./semgrep-rules/ ./src # SAST 全量扫描
semgrep –config auto ${file} # 扫描当前文件
npx eslint –ext .js,.ts ./src –plugin security # ESLint 安全检查
bandit -r ./src -ll # Python 安全扫描
# ===== DAST 扫描 =====
./scripts/dast-workflow.sh http://localhost:3000 # 完整 DAST 流程
python3 scripts/api_security_scanner.py # API 安全测试
# ===== 报告生成 =====
python3 scripts/generate_security_report.py –format html # HTML 报告
python3 scripts/generate_security_report.py –format markdown # MD 报告
# ===== 规则管理 =====
semgrep –config ./semgrep-rules/ –validate # 验证规则语法
semgrep –config ./semgrep-rules/ –test # 运行规则测试
semgrep –config ./semgrep-rules/ –dryrun ./src # 试运行
# ===== CI/CD =====
semgrep –config auto –sarif -o results.sarif . # SARIF 格式输出
semgrep –config auto –json -o results.json . # JSON 格式输出
semgrep –config auto –error –severity ERROR . # 仅 ERROR 阻塞
# ===== 故障排查 =====
semgrep –version # 检查版本
semgrep –config ./rules/ –debug ./src 2>&1 | head -50 # 调试模式
python3 -c "import yaml; yaml.safe_load(open('rules.yaml'))" # YAML 验证
本文完
本教程版本:v1.0 | 最后更新:2026 年 8 月
适用工具版本:Cursor 0.40+, Semgrep 1.70+, OWASP ZAP 2.15+, Node.js 20 LTS, Python 3.11+
如有问题或建议,欢迎在团队安全频道中交流讨论。安全是一个持续改进的过程,愿每一位开发者都能编写出安全、可靠的代码。


