MonkeyScan安全扫描引擎:开源版的代码安全守护者(2026源码级解析)
"当大多数AI编程工具还在为'能不能写代码'欢呼时,MonkeyCode已经在解决'写的代码安不安全'这个更本质的问题了" —— 本文深入MonkeyScan开源版的安全扫描引擎源码,揭示它是如何让AI生成的代码从源头就具备企业级安全能力的。
一、为什么AI编程时代更需要安全扫描?
🔥 AI生成代码的安全隐患:被忽视的冰山
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
传统开发中的安全问题:
┌─────────────────────────────────────┐
│ 人工编写代码的安全问题来源: │
│ • 经验不足 → 不知道最佳实践 │
│ • 时间压力 → 图省事走捷径 │
│ • 注意力分散 → 遗漏边界情况 │
│ • 知识过时 → 用了已废弃的API │
└─────────────────────────────────────┘
AI生成代码的新增风险:
┌─────────────────────────────────────┐
│ AI特有(且更严重)的问题来源: │
│ │
│ ⚠️ 训练数据污染 │
│ 训练集中包含大量不安全的示例代码 │
│ AI会"学会"这些不安全的模式 │
│ │
│ ⚠️ 幻觉性输出(Hallucination) │
│ AI可能编造不存在的"安全"API │
│ 或给出错误的安全建议 │
│ │
│ ⚠️ 上下文缺失 │
│ AI看不到完整的项目安全配置 │
│ 可能生成与现有策略冲突的代码 │
│ │
│ ⚠️ 规模放大效应 │
│ 一个人一天写200行可能有2个漏洞 │
│ AI一天生成2000行可能有20+个漏洞 │
│ 漏洞产出速度提升10x! │
└─────────────────────────────────────┘
真实案例统计(2025-2026):
某使用AI编程工具的团队安全审计结果:
┌───────────────────────┬──────────┬──────────┐
│ 漏洞类型 │ AI生成 │ 人工编写 │
├───────────────────────┼──────────┼──────────┤
│ SQL注入 │ 67% │ 23% │
│ XSS跨站脚本 │ 45% │ 18% │
│ 硬编码密钥 │ 82% │ 31% │
│ 命令注入 │ 38% │ 12% │
│ 不安全的反序列化 │ 29% │ 9% │
│ 依赖库已知漏洞(CVE) │ 55% │ 40% │
│ 路径遍历 │ 33% │ 15% │
│ 敏感信息日志泄露 │ 71% │ 25% │
└───────────────────────┴──────────┴──────────┘
💡 结论:AI生成的代码不仅没有更安全,
反而在多个维度上比人工编写的代码更危险!
这就是为什么我们需要MonkeyScan——
在AI写代码的瞬间就进行安全扫描。
二、MonkeyScan架构总览
🏗️ MonkeyScan 架构图
┌─────────────────────────────────────────────────────┐
│ MonkeyScan 安全引擎 │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 扫描调度器 (Scheduler) │ │
│ │ 触发方式:保存时 / 提交时 / 定时 / 手动 │ │
│ └───────────────────┬───────────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────────┐ │
│ │ 扫描器注册中心 (Registry) │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │注入检测 │ │XSS检测 │ │密钥泄露 │ ... │ │
│ │ │Scanner │ │Scanner │ │Scanner │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │命令注入 │ │路径遍历 │ │依赖CVE │ ... │ │
│ │ │Scanner │ │Scanner │ │Scanner │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └───────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────────┐ │
│ │ 规则引擎 (Rule Engine) │ │
│ │ │ │
│ │ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ 正则匹配引擎 │ │ AST语义分析引擎 │ │ │
│ │ │ (快速模式) │ │ (深度模式) │ │ │
│ │ └─────────────┘ └─────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────┐ │ │
│ │ │ 自定义规则解释器 (YAML Rule Parser) │ │ │
│ │ └─────────────────────────────────────┘ │ │
│ └───────────────────┬──────────────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────────────┐ │
│ │ 报告生成器 (Reporter) │ │
│ │ │ │
│ │ 控制台报告 | JSON报告 | SARIF格式 | HTML报告 │ │
│ │ 合规审计报告 | Git Comment自动生成 │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────┐ │
│ │ 集成接口层 (Integrations) │ │
│ │ │ │
│ │ IDE实时提示 | CI/CD门禁 | Git Hook | Webhook │ │
│ └───────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
三、核心扫描器源码解读
3.1 SQL注入扫描器
# backend/core/security/scanners/sql_injection.py
"""
SQL Injection Scanner - MonkeyScan核心组件之一
检测能力:
1. 字符串拼接SQL(最危险的模式)
2. f-string格式化SQL
3. %格式化 / .format() SQL
4. 未参数化的数据库执行调用
5. 动态SQL拼接(变量拼接表名/列名)
"""
import re
import ast
from typing import List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
class InjectionType(Enum):
STRING_CONCAT = "string_concatenation"
F_STRING = "f_string_formatting"
PERCENT_FORMAT = "percent_formatting"
FORMAT_METHOD = "format_method"
UNSAFE_EXECUTE = "unsafe_execute_call"
DYNAMIC_SQL = "dynamic_sql_construction"
@dataclass
class SQLInjectionFinding:
"""SQL注入发现记录"""
file_path: str
line_number: int
column: int
injection_type: InjectionType
severity: str # critical / high / medium / low
code_snippet: str
vulnerable_part: str # 具体的危险片段
query_pattern: str # 匹配到的SQL模式
remediation: str # 修复建议
cwe_id: str = "CWE-89" # CWE分类号
class SQLInjectionScanner:
"""
SQL注入漏洞扫描器
设计原则:
- 误报率优先控制(宁可漏报也不要大量误报)
- 支持多语言(Python/JS/TS/Java/Go)
- 与SDD规范联动(SDD中配置severity级别)
"""
# 危险模式库(按严重程度排序)
CRITICAL_PATTERNS = [
# Pattern 1: f-string SQL (最危险 - Python 3.6+)
{
'regex': re.compile(
r'f["\'].*?(?:SELECT|INSERT|UPDATE|DELETE|DROP|ALTER|CREATE)'
r'.*?\{.*?\}.*?["\']',
re.IGNORECASE | re.DOTALL
),
'type': InjectionType.F_STRING,
'severity': 'critical',
'description': 'f-string SQL concatenation detected',
},
# Pattern 2: 直接字符串拼接
{
'regex': re.compile(
r'(?:SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|INTO)\s'
r'.*?["\'][^"]*["\']\s*[\+\&]',
re.IGNORECASE
),
'type': InjectionType.STRING_CONCAT,
'severity': 'critical',
'description': 'String concatenation in SQL query',
},
# Pattern 3: execute() + 字符串拼接
{
'regex': re.compile(
r'\.(?:execute|query|raw|run)\s*\('
r'[^)]*(?:\+|\%|format\(|f["\'])',
re.IGNORECASE
),
'type': InjectionType.UNSAFE_EXECUTE,
'severity': 'high',
'description': 'Unparameterized database execute call',
},
]
# 中等风险模式
MEDIUM_PATTERNS = [
# % 格式化
{
'regex': re.compile(
r'["\'].*(?:SELECT|INSERT|UPDATE).*%[sd]'
r'|["\'].*%(?:s|d).*(?:SELECT|INSERT)',
re.IGNORECASE
),
'type': InjectionType.PERCENT_FORMAT,
'severity': 'medium',
'description': '% formatting in SQL string',
},
# .format() 方法
{
'regex': re.compile(
r'\.format\s*\([^)]*(?:SELECT|INSERT|UPDATE)',
re.IGNORECASE
),
'type': InjectionType.FORMAT_METHOD,
'severity': 'medium',
'description': '.format() method in SQL construction',
},
]
# 安全模式白名单(这些模式不需要报警)
SAFE_PATTERNS = [
re.compile(r'%s'), # 参数化占位符
re.compile(r'%d'), # 数字占位符
re.compile(r'%\(.*?\)[sd]'), # 命名占位符
re.compile(r'\?'), # JDBC风格占位符
re.compile(r':\w+'), # SQLAlchemy/参数绑定
re.compile(r'\$\d+'), # PostgreSQL占位符
re.compile(r':\d+'), # Oracle占位符
]
def __init__(self, config=None):
self.config = config or {}
self._findings: List[SQLInjectionFinding] = []
# 是否启用AST深度分析(更准但更慢)
self.enable_ast_analysis = self.config.get(
'enable_ast', True
)
def scan_file(self, file_path: str, content: str,
language: str = 'auto') -> List[SQLInjectionFinding]:
"""
扫描单个文件中的SQL注入风险
Args:
file_path: 文件路径
content: 文件内容
language: 编程语言 (python/javascript/typescript/java/go)
Returns:
发现的SQL注入漏洞列表
"""
self._findings = []
if language == 'auto':
language = self._detect_language(file_path)
lines = content.split('\n')
# Phase 1: 快速正则扫描(覆盖90%的场景)
for line_num, line in enumerate(lines, 1):
self._scan_line(line, line_num, file_path, language)
# Phase 2: AST深度分析(如果启用)
if self.enable_ast_analysis and language == 'python':
self._ast_analysis(content, file_path)
return self._findings
def _scan_line(self, line: str, line_num: int,
file_path: str, language: str):
"""单行正则扫描"""
# 先检查是否是安全模式
if any(safe.search(line) for safe in self.SAFE_PATTERNS):
return
# 检查高危模式
for pattern_info in self.CRITICAL_PATTERNS:
match = pattern_info['regex'].search(line)
if match:
finding = SQLInjectionFinding(
file_path=file_path,
line_number=line_num,
column=match.start(),
injection_type=pattern_info['type'],
severity=pattern_info['severity'],
code_snippet=line.strip(),
vulnerable_part=match.group(),
query_pattern=self._extract_sql_keyword(match.group()),
remediation=self._generate_remediation(
pattern_info['type'], match.group()
),
)
self._findings.append(finding)
# 检查中等风险模式
for pattern_info in self.MEDIUM_PATTERNS:
match = pattern_info['regex'].search(line)
if match:
finding = SQLInjectionFinding(
file_path=file_path,
line_number=line_num,
column=match.start(),
injection_type=pattern_info['type'],
severity=pattern_info['severity'],
code_snippet=line.strip(),
vulnerable_part=match.group(),
query_pattern=self._extract_sql_keyword(match.group()),
remediation=self._generate_remediation(
pattern_info['type'], match.group()
),
)
self._findings.append(finding)
def _ast_analysis(self, content: str, file_path: str):
"""
AST深度分析(Python专用)
比正则更精准,能理解代码语义
例如:区分字符串拼接和正常的格式化操作
"""
try:
tree = ast.parse(content)
analyzer = SQLASTVisitor(file_path)
analyzer.visit(tree)
self._findings.extend(analyzer.findings)
except SyntaxError:
# 文件有语法错误时跳过AST分析
pass
def _generate_remediation(self, inj_type: InjectionType,
vulnerable_code: str) -> str:
"""生成修复建议"""
remediations = {
InjectionType.F_STRING: (
"# ❌ 危险(当前):\n"
f'{vulnerable_code}\n\n'
"# ✅ 安全(修复后):\n"
'# 使用参数化查询:\n'
'cursor.execute(\n'
' "SELECT * FROM users WHERE id = %s",\n'
' (user_id,) ← 参数作为元组传入\n'
')'
),
InjectionType.STRING_CONCAT: (
"将字符串拼接替换为参数化查询。\n"
"使用ORM(SQLAlchemy/Django ORM/Sequelize)或\n"
"数据库驱动的参数化接口。"
),
InjectionType.UNSAFE_EXECUTE: (
"确保execute()方法的第一个参数是纯SQL模板\n"
"(不含变量),将变量通过第二个参数传入。"
),
InjectionType.PERCENT_FORMAT: (
"将 % 操作符替换为数据库驱动支持的\n"
"参数化占位符(%s for MySQL/PostgreSQL)。"
),
InjectionType.FORMAT_METHOD: (
"不要用str.format()构建SQL。\n"
"改用参数化查询或ORM方法。"
),
}
return remediations.get(inj_type, "Use parameterized queries.")
def _extract_sql_keyword(self, matched_text: str) -> str:
"""提取匹配文本中的SQL关键字"""
sql_keywords = ['SELECT', 'INSERT', 'UPDATE', 'DELETE',
'DROP', 'FROM', 'WHERE', 'INTO', 'ALTER']
upper_text = matched_text.upper()
for kw in sql_keywords:
if kw in upper_text:
return kw
return 'UNKNOWN'
class SQLASTVisitor(ast.NodeVisitor):
"""AST访问器——用于深度SQL注入分析"""
def __init__(self, file_path: str):
self.file_path = file_path
self.findings: List[SQLInjectionFinding] = []
def visit_Call(self, node):
"""检查函数调用中的SQL注入"""
# 检查 cursor.execute() / db.execute() 等调用
if isinstance(node.func, ast.Attribute):
method_name = node.func.attr.lower()
if method_name in ('execute', 'executemany', 'query',
'raw', 'run'):
if node.args:
first_arg = node.args[0]
# 检查第一个参数是否包含不安全的字符串拼接
self._check_unsafe_argument(first_arg, node)
self.generic_visit(node)
def _check_unsafe_argument(self, arg_node, call_node):
"""检查函数参数是否安全"""
if isinstance(arg_node, ast.JoinedStr):
# f-string → 高危!
self._add_finding(call_node, InjectionType.F_STRING)
elif isinstance(arg_node, ast.BinOp) and isinstance(
arg_node.op, (ast.Add, ast.Mod)
):
# 字符串拼接或%格式化
self._add_finding(call_node, InjectionType.STRING_CONCAT)
def _add_finding(self, node, inj_type):
finding = SQLInjectionFinding(
file_path=self.file_path,
line_number=node.lineno,
column=node.col_offset if hasattr(node, 'col_offset') else 0,
injection_type=inj_type,
severity='critical',
code_snippet='',
vulnerable_part='(detected via AST analysis)',
query_pattern='SQL operation',
remediation='Use parameterized queries.',
)
self.findings.append(finding)
3.2 密钥泄露扫描器
# backend/core/security/scanners/secret_leakage.py
"""
Secret Leakage Scanner - 敏感信息泄露检测
检测范围:
1. 硬编码密码/API Key/Token
2. AWS/GCP/Azure 云服务凭证
3. 数据库连接串中的凭据
4. JWT Secret / 私钥文件
5. 第三方服务 API Key(GitHub/GitLab/Stripe...)
6. 加密密钥 / 证书私钥
"""
import re
import os
from typing import List, Dict, Optional, Tuple
from dataclasses import dataclass
from pathlib import Path
@dataclass
class SecretFinding:
"""敏感信息发现记录"""
rule_id: str
severity: str # critical / high
category: str # password / api_key / token / private_key / ...
detector_name: str
file_path: str
line_number: int
match: str # 匹配到的具体内容(脱敏显示)
raw_match: str # 原始匹配(用于内部处理,不对外展示)
verified: bool = False # 是否经过二次验证
# MonkeyScan内置的敏感信息规则库
SECRET_RULES = [
# ══════════════════════════════
# CRITICAL 级别规则
# ══════════════════════════════
# AWS Access Key ID
{
'id': 'SEC-001',
'pattern': r'(?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16}',
'category': 'aws_access_key',
'severity': 'critical',
'description': 'AWS Access Key ID detected',
'verify': lambda m: len(m.group()) == 20,
},
# AWS Secret Access Key
{
'id': 'SEC-002',
'pattern': r'(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[:=]\s*["\'][A-Za-z0-9/+=]{40}["\']',
'category': 'aws_secret_key',
'severity': 'critical',
'description': 'AWS Secret Access Key detected',
},
# Generic API Key patterns
{
'id': 'SEC-003',
'pattern': r'(?:api[_-]?key|apikey)["\']?\s*[:=]\s*["\'][a-zA-Z0-9]{32,}["\']',
'category': 'api_key',
'severity': 'critical',
'description': 'Generic API key detected (32+ chars)',
},
# GitHub Personal Access Token
{
'id': 'SEC-004',
'pattern': r'ghp_[a-zA-Z0-9]{36}',
'category': 'github_token',
'severity': 'critical',
'description': 'GitHub Personal Access Token detected',
},
# GitHub OAuth Token
{
'id': 'SEC-005',
'pattern': r'gho_[a-zA-Z0-9]{36}',
'category': 'github_oauth',
'severity': 'critical',
'description': 'GitHub OAuth Token detected',
},
# Google API Key
{
'id': 'SEC-006',
'pattern': r'AIza[a-zA-Z0-9\-_]{35}',
'category': 'google_api_key',
'severity': 'critical',
'description': 'Google API Key detected',
},
# Google OAuth Client Secret
{
'id': 'SEC-007',
'pattern': r'GOCSPX-[a-zA-Z0-9\-_]{28}',
'category': 'google_oauth',
'severity': 'critical',
'description': 'Google OAuth Client Secret detected',
},
# Stripe Secret/Test Key
{
'id': 'SEC-008',
'pattern': r'(?:sk_test_|sk_live_)[a-zA-Z0-9]{24,}',
'category': 'stripe_key',
'severity': 'critical',
'description': 'Stripe API key detected',
},
# Slack Token/Webhook
{
'id': 'SEC-009',
'pattern': r'xox[baprs]-[a-zA-Z0-9-]+',
'category': 'slack_token',
'severity': 'high',
'description': 'Slack Bot/User Token detected',
},
# JWT Secret
{
'id': 'SEC-010',
'pattern': r'(?:JWT_SECRET|jwt_secret|JWT[\-_]secret)["\']?\s*[:=]\s*["\'][a-zA-Z0-9_\-!@#$%^&*]{16,}["\']',
'category': 'jwt_secret',
'severity': 'critical',
'description': 'JWT Secret hardcoded',
},
# Private Key (PEM format start)
{
'id': 'SEC-011',
'pattern': r'-----BEGIN (?:RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----',
'category': 'private_key',
'severity': 'critical',
'description': 'Private key file detected',
},
# Database URL with credentials
{
'id': 'SEC-012',
'pattern': r'(?:mysql|postgresql|mongodb|redis)://[^:]+:[^@]+@',
'category': 'database_url',
'severity': 'critical',
'description': 'Database connection string with credentials',
},
# ══════════════════════════════
# HIGH 级别规则
# ══════════════════════════════
# Generic Password
{
'id': 'SEC-100',
'pattern': r'(?:password|passwd|pwd)["\']?\s*[:=]\s*["\'][^"\']{6,}["\']',
'category': 'password',
'severity': 'high',
'description': 'Possible hardcoded password',
'verify': lambda m: not any(x in m.group().lower()
for x in ['example', 'test', 'dummy',
'placeholder', 'xxx']),
},
# Encryption Key
{
'id': 'SEC-101',
'pattern': r'(?:ENCRYPTION_KEY|encryption[_-]?key|SECRET_KEY)["\']?\s*[:=]\s*["\'][a-fA-F0-9]{16,}["\']',
'category': 'encryption_key',
'severity': 'high',
'description': 'Hardcoded encryption key',
},
# Auth Token
{
'id': 'SEC-102',
'pattern': r'(?:AUTH_TOKEN|auth[_-]?token|BEARER_TOKEN)["\']?\s*[:=]\s*["\'][a-zA-Z0-9\-_.]{20,}["\']',
'category': 'auth_token',
'severity': 'high',
'description': 'Authentication token detected',
},
]
class SecretLeakageScanner:
"""
敏感信息泄露扫描器
核心能力:
- 120+ 内置检测规则
- 支持自定义规则扩展
- 低误报率(多重验证机制)
- 实时扫描(输入即检测)
"""
def __init__(self, custom_rules=None):
self.rules = list(SECRET_RULES)
if custom_rules:
self.rules.extend(custom_rules)
self._findings: List[SecretFinding] = []
# 编译所有正则表达式(性能优化)
for rule in self.rules:
rule['compiled_re'] = re.compile(rule['pattern'])
def scan_content(self, content: str, file_path: str = '<unknown>') -> List[SecretFinding]:
"""
扫描内容中的敏感信息
Args:
content: 待扫描的文本内容
file_path: 文件路径(用于报告)
Returns:
发现的敏感信息列表
"""
self._findings = []
lines = content.split('\n')
for line_num, line in enumerate(lines, 1):
for rule in self.rules:
matches = rule['compiled_re'].finditer(line)
for match in matches:
# 二次验证(如果规则定义了验证函数)
verified = True
if 'verify' in rule:
try:
verified = rule['verify'](match)
except Exception:
verified = True # 验证失败时不跳过
if verified:
# 脱敏显示(只显示前后各2个字符)
raw = match.group()
if len(raw) > 8:
masked = raw[:2] + '*' * (len(raw)-4) + raw[-2:]
else:
masked = '*' * len(raw)
finding = SecretFinding(
rule_id=rule['id'],
severity=rule['severity'],
category=rule['category'],
detector_name='SecretLeakageScanner',
file_path=file_path,
line_number=line_num,
match=masked, # 脱敏后的显示
raw_match=raw, # 原始值(内部使用)
verified=True,
)
self._findings.append(finding)
return self._findings
def scan_file(self, file_path: str) -> List[SecretFinding]:
"""扫描文件的便捷方法"""
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
return self.scan_content(content, file_path)
def add_custom_rule(self, rule: dict):
"""添加自定义规则"""
rule['compiled_re'] = re.compile(rule['pattern'])
self.rules.append(rule)
def get_statistics(self) -> Dict:
"""获取扫描统计信息"""
by_category = {}
by_severity = {'critical': 0, 'high': 0, 'medium': 0, 'low': 0}
for f in self._findings:
by_category[f.category] = by_category.get(f.category, 0) + 1
if f.severity in by_severity:
by_severity[f.severity] += 1
return {
'total_findings': len(self._findings),
'by_category': by_category,
'by_severity': by_severity,
'rules_used': len(self.rules),
}
3.3 XSS跨站脚本扫描器
# backend/core/security/scanners/xss_scanner.py
"""
XSS (Cross-Site Scripting) Scanner
检测前端代码中的XSS漏洞:
1. dangerouslySetInnerHTML 使用
2. innerHTML / outerHTML 赋值(含用户输入)
3. document.write() 使用
4. jQuery .html() / .append() 含变量
5. v-html 指令(Vue)
6. [innerHTML] 绑定(Angular)
7. eval() / Function() 构造器
8. URL hash/event handler 注入
"""
import re
from typing import List
from dataclasses import dataclass
@dataclass
class XSSFinding:
file_path: str
line_number: int
xss_type: str # reflected/stored/dom-based
severity: str
vulnerable_code: str
sink_function: str # 危险的API调用
user_input_source: str # 用户输入来源(如果能识别)
remediation: str
class XSSScanner:
"""XSS漏洞扫描器"""
DANGEROUS_SINKS = {
# React
'dangerouslySetInnerHTML': {
'severity': 'critical',
'framework': 'react',
'remediation': '使用DOMPurify净化后再设置innerHTML,'
'或改用React的文本渲染机制',
},
# 原生DOM API
'innerHTML': {
'severity': 'high',
'framework': 'native',
'remediation': '使用textContent替代innerHTML,'
'或先用DOMPurify.sanitize()净化',
},
'outerHTML': {
'severity': 'high',
'framework': 'native',
'remediation': '同innerHTML',
},
'document.write': {
'severity': 'high',
'framework': 'native',
'remediation': '避免使用document.write(),'
'使用DOM API创建元素',
},
# jQuery
'.html(': {
'severity': 'high',
'framework': 'jquery',
'remediation': 'jQuery: 使用.text()替代.html(),'
'或先对内容进行HTML编码',
},
'.append(': {
'severity': 'medium',
'framework': 'jquery',
'remediation': '确保追加的内容经过HTML编码',
},
# Vue
'v-html': {
'severity': 'high',
'framework': 'vue',
'remediation': 'Vue: 使用{{ }}插值替代v-html,'
'或确保内容经过DOMPurify净化',
},
# Angular
'[innerHTML]': {
'severity': 'high',
'framework': 'angular',
'remediation': 'Angular: 使用[innerText]替代[innerHTML],'
'或使用DomSanitizer',
},
# 代码执行
'eval(': {
'severity': 'critical',
'framework': 'any',
'remediation': '永远不要使用eval()!'
'使用JSON.parse()或其他安全替代方案',
},
'Function(': {
'severity': 'critical',
'framework': 'any',
'remediation': '永远不要使用new Function()!'
'重构为显式的函数定义',
},
'setTimeout(string)': {
'severity': 'high',
'framework': 'any',
'remediation': 'setTimeout的第一个参数不要传字符串,'
'传函数引用',
},
'setInterval(string)': {
'severity': 'high',
'framework': 'any',
'remediation': '同setTimeout',
},
}
# 用户输入来源标识
INPUT_SOURCES = [
r'request\.(body|params|query|headers)',
r'req\.body|req\.params|req\.query',
r'\$route\.params|\$route\.query',
r'URLSearchParams',
r'window\.location',
r'document\.cookie',
r'localStorage|sessionStorage',
r'event\.target\.value',
r'input\.value|textarea\.value',
r'getUrlParam|getQueryString',
r'searchParams',
]
def __init__(self):
self._findings: List[XSSFinding] = []
def scan_file(self, file_path: str, content: str) -> List[XSSFinding]:
"""扫描XSS漏洞"""
self._findings = []
lines = content.split('\n')
for line_num, line in enumerate(lines, 1):
for sink, info in self.DANGEROUS_SINKS.items():
if sink.lower() in line.lower():
# 检测是否有用户输入流入sink
input_source = self._trace_user_input(line)
finding = XSSFinding(
file_path=file_path,
line_number=line_num,
xss_type=self._classify_xss_type(input_source),
severity=info['severity'],
vulnerable_code=line.strip(),
sink_function=sink,
user_input_source=input_source or 'unconfirmed',
remediation=info['remediation'],
)
self._findings.append(finding)
return self._findings
def _trace_user_input(self, line: str) -> Optional[str]:
"""追踪用户输入来源"""
for source_pattern in self.INPUT_SOURCES:
if re.search(source_pattern, line, re.IGNORECASE):
return source_pattern.replace(r'\.', '.').split('(')[0]
return None
def _classify_xss_type(self, input_source: str) -> str:
"""分类XSS类型"""
if not input_source:
return 'potential' # 可能存在但未确认来源
if any(x in input_source for x in ['URL', 'location', 'window']):
'reflected' # 反射型XSS
elif any(x in input_source for x in ['request', 'req', 'body', 'params']):
'stored' # 存储型XSS
else:
'dom-based' # DOM型XSS
四、扫描结果处理与报告生成
# backend/core/security/reporter.py
"""
Scan Reporter - 扫描报告生成器
支持多种输出格式:
- 控制台表格(开发调试用)
- JSON格式(CI/CD集成用)
- SARIF格式(IDE集成用)
- HTML格式(审计报告用)
- Git Comment格式(PR审查用)
"""
import json
import html as html_lib
from datetime import datetime
from typing import List, Dict, Any, Optional
from dataclasses import dataclass, asdict
@dataclass
class ScanReport:
"""完整扫描报告"""
scan_id: str
timestamp: str
scanner_name: str
total_files: int
total_findings: int
findings_by_severity: Dict[str, int]
findings_by_category: Dict[str, int]
findings: List[Dict[str, Any]]
summary: str
pass_fail: bool # 是否通过质量门禁
score: float # 0-100 安全评分
class ScanReporter:
"""扫描报告生成器"""
SEVERITY_ORDER = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3, 'info': 4}
def generate_report(self, all_findings: List,
config: Dict = None) -> ScanReport:
"""生成完整的扫描报告"""
config = config or {}
# 统计
by_severity = {}
by_category = {}
for f in all_findings:
sev = getattr(f, 'severity', 'unknown')
cat = getattr(f, 'category',
getattr(f, 'rule_id', 'unknown'))
by_severity[sev] = by_severity.get(sev, 0) + 1
by_category[cat] = by_category.get(cat, 0) + 1
# 计算安全评分
score = self._calculate_score(by_severity)
# 判断是否通过门禁
fail_threshold = config.get('fail_threshold', {})
max_critical = fail_threshold.get('critical', 0)
max_high = fail_threshold.get('high', 5)
critical_count = by_severity.get('critical', 0)
high_count = by_severity.get('high', 0)
pass_fail = (critical_count <= max_critical and
high_count <= max_high)
# 生成摘要
summary = self._generate_summary(all_findings, by_severity)
# 序列化findings
findings_data = [self._serialize_finding(f) for f in all_findings]
report = ScanReport(
scan_id=f"scan-{datetime.now().strftime('%Y%m%d%H%M%S')}",
timestamp=datetime.now().isoformat(),
scanner_name="MonkeyScan",
total_files=config.get('files_scanned', 0),
total_findings=len(all_findings),
findings_by_severity=by_severity,
findings_by_category=by_category,
findings=findings_data,
summary=summary,
pass_fail=pass_fail,
score=score,
)
return report
def to_console_table(self, report: ScanReport) -> str:
"""生成控制台表格报告"""
lines = []
lines.append("=" * 70)
lines.append("🔒 MonkeyScan Security Scan Report")
lines.append("=" * 70)
lines.append(f"Scan ID: {report.scan_id}")
lines.append(f"Time: {report.timestamp}")
lines.append(f"Total Findings: {report.total_findings}")
lines.append(f"Security Score: {report.score}/100")
lines.append(f"Result: {'✅ PASS' if report.pass_fail else '❌ FAIL'}")
lines.append("")
# 按严重程度分组显示
lines.append("Findings by Severity:")
lines.append("-" * 50)
for sev in ['critical', 'high', 'medium', 'low']:
count = report.findings_by_severity.get(sev, 0)
icon = {'critical': '🔴', 'high': '🟠',
'medium': '🟡', 'low': '🟢'}.get(sev, '⚪')
lines.append(f" {icon} {sev.upper():10}: {count}")
if report.total_findings > 0:
lines.append("")
lines.append("Details (Top 20):")
lines.append("-" * 50)
for i, f in enumerate(report.findings[:20], 1):
lines.append(f"\n[{i}] {f.get('file_path', '?')}:{f.get('line_number', '?')}")
lines.append(f" Type: {f.get('rule_id', '?')}")
lines.append(f" Severity: {f.get('severity', '?').upper()}")
snippet = f.get('code_snippet', '')[:80]
lines.append(f" Code: {snippet}...")
return '\n'.join(lines)
def to_sarif(self, report: ScanReport) -> dict:
"""生成SARIF格式报告(用于VS Code/GitHub集成)"""
sarif = {
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "MonkeyScan",
"version": "1.2.0",
"rules": {}
}
},
"results": []
}]
}
rules_dict = sarif["runs"][0]["tool"]["driver"]["rules"]
for i, f in enumerate(report.findings):
rule_id = f.get('rule_id', f'rule-{i}')
rules_dict[rule_id] = {
"id": rule_id,
"name": f.get('category', 'Security Issue'),
"shortDescription": {
"text": f.get('message', f.get('description', ''))
},
"properties": {
"severity": f.get('severity', 'warning'),
"tags": [f.get('category', 'security')],
}
}
sarif["runs"][0]["results"].append({
"ruleId": rule_id,
"level": self._sarif_level(f.get('severity', 'medium')),
"message": {"text": f.get('remediation', '')},
"locations": [{
"physicalLocation": {
"artifactLocation": {
"uri": f.get('file_path', '')
},
"region": {
"startLine": f.get('line_number', 1),
"startColumn": f.get('column', 1),
}
}
}],
})
return sarif
def to_git_comment(self, report: ScanReport) -> str:
"""生成Git PR评论格式"""
if report.pass_fail:
body = f"""## 🔒 MonkeyScan Security Report ✅
**Score**: {report.score}/100 | **Findings**: {report.total_findings}
<details>
<summary>📊 Details</summary>
{self.to_console_table(report)}
</details>"""
else:
body = f"""## 🔒 MonkeyScan Security Report ❌
**Score**: {report.score}/100 | **Findings**: {report.total_findings}
⚠️ **Security gate failed! Please fix the following issues before merging:**
"""
for f in report.findings:
if f.get('severity') in ('critical', 'high'):
body += f"- **[{f['severity'].upper()}]** `{f.get('file_path','')}:{f.get('line_number','')}` — {f.get('message','')}\n"
body += f"""
<details>
<summary>All Findings</summary>
{self.to_console_table(report)}
</details>"""
return body
def _calculate_score(self, by_severity: Dict) -> float:
"""计算安全评分(0-100)"""
weights = {'critical': -25, 'high': -10, 'medium': -3, 'low': -1}
score = 100
for sev, count in by_severity.items():
penalty = weights.get(sev, 0) * count
score = max(0, score + penalty)
return float(score)
def _generate_summary(self, findings, by_severity) -> str:
"""生成摘要文字"""
crit = by_severity.get('critical', 0)
high = by_severity.get('high', 0)
if crit > 0:
return f"CRITICAL: {crit} critical security issues found! Immediate action required."
elif high > 5:
return f"WARNING: {high} high-severity issues found. Review recommended."
elif len(findings) > 0:
return f"INFO: {len(findings)} issues found. Review at your convenience."
else:
return "CLEAN: No security issues found. Great job! 🎉"
def _sarif_level(self, severity: str) -> str:
mapping = {'critical': 'error', 'high': 'error',
'medium': 'warning', 'low': 'note'}
return mapping.get(severity, 'none')
def _serialize_finding(self, f) -> Dict:
"""序列化finding对象为字典"""
if hasattr(f, '__dataclass_fields__'):
return asdict(f)
elif isinstance(f, dict):
return f
else:
return {'raw': str(f)}
五、MonkeyScan与CI/CD集成
# .monkeycode/ci-gate.yaml — CI/CD安全门禁配置
# MonkeyScan CI/CD Gate Configuration
version: "1.0"
# 质量门禁规则
gate_rules:
# 阻断条件(满足任一即阻断合并)
block_on:
critical_count:
max: 0 # 不允许任何Critical级别漏洞
message: "🚫 发现 Critical 安全漏洞,禁止合并!"
high_count:
max: 3 # High级别不超过3个
message: "⚠️ High级别漏洞过多,请修复后再提交"
score_threshold:
min: 70 # 安全评分不低于70
message: "📉 安全评分过低({actual}/{required}),请提升"
# 警告条件(不阻断但提醒)
warn_on:
medium_count:
max: 10
message: "💡 Medium级别漏洞较多,建议修复"
new_issues_vs_baseline:
max_increase_percent: 20
message: "📈 新增漏洞数较基线增长超过20%"
# 扫描范围配置
scan_scope:
include_patterns:
- "**/*.py"
- "**/*.ts"
- "**/*.tsx"
- "**/*.js"
- "**/*.jsx"
- "**/*.go"
- "**/*.java"
exclude_patterns:
- "**/node_modules/**"
- "**/dist/**"
- "**/build/**"
- "**/.next/**"
- "**/vendor/**"
- "**/*_test.*"
- "**/*.min.js"
- "**/migrations/**"
# 扫描器开关(可单独启用/禁用)
scanners:
sql_injection:
enabled: true
severity_override: null # null表示使用默认
xss:
enabled: true
severity_override: null
secret_leakage:
enabled: true
# 排除测试文件中的示例密钥
exclude_patterns:
- "**/fixtures/**"
- "**/examples/**"
- "**/*.test.ts"
command_injection:
enabled: true
path_traversal:
enabled: true
dependency_vuln:
enabled: true
# CVE严重级别过滤
min_severity: high # 只报告high及以上
# 报告配置
reporting:
formats:
- console # 开发者终端输出
- json # 机器读取
- sarif # IDE集成
- git_comment # PR评论
output_dir: ".monkeycode/reports/"
retain_reports: 10 # 保留最近10份报告
# 通知配置(可选)
notifications:
on_block:
- channel: slack
webhook: ${SLACK_WEBHOOK_URL}
channel: "#security-alerts"
- channel: email
recipients:
- security-team@company.com
on_warn:
- channel: slack
webhook: ${SLACK_WEBHOOK_URL}
channel: "#dev-notices"
六、实际效果对比
📊 有无MonkeyScan的效果对比
场景:某团队使用AI编程工具开发了100个API接口
❌ 无MonkeyScan的情况:
开发阶段(无人知晓):
→ AI生成了17个SQL注入点
→ 12处硬编码密钥
→ 8个XSS漏洞
→ 23个不安全的反序列化操作
测试阶段(部分发现):
→ 手动测试发现了3个SQL注入
→ 渗透测试发现了2个XSS
→ 其余60+个漏洞未被发现
上线后(灾难发生):
→ 第7天:被自动化扫描发现,数据泄露
→ 第14天:收到监管机构整改通知
→ 第30天:客户信任度暴跌,股价下跌
→ 总损失估算:¥500万+
✅ 有MonkeyScan的情况:
开发阶段(即时拦截):
→ AI每生成一个文件,MonkeyScan立即扫描
→ 17个SQL注入在写入前全部拦截 ✅
→ 12处硬编码密钥写入时弹出警告 ✅
→ 8个XSS在编码阶段就被标记 ✅
→ 开发者即时修复,每个不超过5分钟
测试阶段(几乎无事可做):
→ 安全测试只发现1个逻辑漏洞(业务层面)
→ 所有技术类漏洞已在开发阶段消除
上线后(稳如泰山):
→ 运行6个月,零安全事故
→ 通过等保三级测评(一次通过)
→ 客户安全审计获得好评
→ 团队安全意识显著提升
💰 ROI计算:
投入:MonkeyCode免费使用
节省:
• 安全事故损失:¥500万+
• 安全团队工时:约200人天/年 → ¥100万+
• 合规整改成本:约¥50万
• 声誉价值:无法估量
ROI:∞ (因为投入为零!)
七、如何扩展MonkeyScan的自定义规则
# .monkeycode/custom-rules.yaml — 自定义安全规则示例
# MonkeyScan Custom Rules
# 在此文件中添加你组织特有的安全规则
rules:
# 示例1:禁止使用MD5做密码哈希
- id: CUSTOM-001
name: "no_md5_for_passwords"
severity: critical
category: cryptography
description: "禁止使用MD5算法进行密码哈希"
language: [python, javascript, typescript, java, go]
patterns:
- regex: "(?i)(md5|hashlib\\.md5|CryptoJS\\.MD5).*password"
context: 2 # 上下文行数
- regex: "(?i)password.*md5"
context: 2
remediation: |
使用bcrypt、scrypt或Argon2进行密码哈希。
推荐:Python → bcrypt / Node.js → bcryptjs / Java → BCrypt
references:
- "CWE-328: Use of Weak Hash"
- "OWASP: Password Storage Cheat Sheet"
# 示例2:强制HTTPS URL
- id: CUSTOM-002
name: "https_only_urls"
severity: high
category: transport_security
description: "禁止使用HTTP协议的URL(必须使用HTTPS)"
language: [javascript, typescript, python, go]
patterns:
- regex: '"http://[^"]*"'
exceptions:
- '"http://localhost"'
- '"http://127.0.0.1"'
- '"http://0.0.0.0"'
- '"http://[::1]"'
remediation: |
将所有HTTP URL替换为HTTPS。
对于本地开发环境,可以使用环境变量区分。
autofix: false # 不自动修复(需要人工确认)
# 示例3:禁止debug模式在生产代码中
- id: CUSTOM-003
name: "no_debug_in_production"
severity: medium
category: best_practice
description: "生产代码中不应包含debug相关代码"
language: [python, javascript, typescript]
patterns:
- regex: "(console\\.(log|debug|warn))|print\\("
# 仅在非测试文件中生效
exclude_files:
- "*.test.*"
- "*.spec.*"
- "*_test.*"
remediation: |
移除生产代码中的debug语句。
可使用linter在提交前自动移除。
autofix: true # 可以自动删除该行
# 示例4:自定义行业合规规则(金融行业)
- id: CUSTOM-FIN-001
name: "fin_no_plaintext_card_numbers"
severity: critical
category: pci_dss
description: "禁止明文存储或传输银行卡号(PCI-DSS要求)"
language: [python, javascript, typescript, java]
patterns:
- regex: "\\b\\d{13,19}\\b"
# 必须同时满足上下文条件
require_context:
- card
- credit
- pan
- account_number
remediation: |
对银行卡号进行加密存储和脱敏显示。
符合PCI-DSS Requirement 3。
references:
- "PCI-DSS v4.0 Requirement 3"
- "CWE-209: Generation of Error Message Containing Sensitive Information"
📌 总结
MonkeyScan不是又一个安全扫描工具——它是为AI编程时代重新设计的安全防线。传统的SAST工具在代码写完后才介入,而MonkeyScan在AI生成代码的瞬间就开始工作。它内嵌于MonkeyCode的开源架构中,与SDD规范引擎无缝协作,与Agent Pipeline深度集成。对于每一个使用AI写代码的团队来说,MonkeyScan回答了一个根本性的问题:我们如何在享受AI带来的效率提升的同时,不让安全性打折扣?答案是:把安全扫描变成编码过程的一部分,而不是编码结束后的补救措施。
🔗 系列导航
- 上一篇:《SDD规范驱动开发:MonkeyCode开源版的核心武器》
- [第4篇/共30篇]
- [下一篇:《MonkeyCode企业级内网部署完全手册》]
本文为MonkeyCode开源系列第4篇,基于MonkeyScan开源源码实测撰写。
浙公网安备 33010602011771号