P0-8 扩大: do_mode.py finish_worker 全专家插件强制路由 - GUI→XDB, network→NDB, C/C++→SDB, done→Rvr, blocked/failed→Dbg - 证据去重: 已有 xdbSessions/ndbSessions/sdbReports/rvrReviewed 则跳过 P1-GAP17: 事件 emit 规范化 - 新增 7 个事件常量 (TASK_ENTERED, TASK_FINISHED, ENGINE_ENTERED 等) - 全部 emit 调用替换字符串字面量为常量,零残留 - 30 个事件类型常量全部定义且唯一 P1-GAP18: 事件日志原子轮转 - emit 计数器每 128 次检查轮转,避免每次 emit 读文件 - 清除未使用的 _emit_with_completion/_pending_merge_complete - 原子轮转: tempfile+os.replace 保证不损坏 eng 极端接管: 强制调用全部专家插件 (Dbg/XDB/NDB/SDB/Rvr) commands/do.md: 更新为全专家插件路由文档 全量测试: 69 通过, 0 失败 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
176 lines
5.2 KiB
Python
Executable File
176 lines
5.2 KiB
Python
Executable File
"""
|
||
AirSec 安全扫描运行时 — V2 新增组件。
|
||
制品敏感数据扫描 + 自动脱敏 + 误报白名单 + 确认流程 + advisory/blocking 模式。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import re
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
|
||
SECRET_PATTERNS: list[tuple[str, str]] = [
|
||
("api_key", r'(?:api[_-]?key|apikey)\s*[:=]\s*["\']?([A-Za-z0-9_\-]{16,})["\']?'),
|
||
("aws_key", r'AKIA[0-9A-Z]{16}'),
|
||
("private_key", r'-----BEGIN (?:RSA|EC|DSA|OPENSSH) PRIVATE KEY-----'),
|
||
("token", r'(?:token|secret|password)\s*[:=]\s*["\']?([^\s"\']{8,})["\']?'),
|
||
("jwt", r'eyJ[A-Za-z0-9_\-]+\.eyJ[A-Za-z0-9_\-]+\.[A-Za-z0-9_\-]+'),
|
||
("url_credential", r'https?://[^:@]+:([^@]+)@'),
|
||
]
|
||
|
||
ALLOWLIST_PATTERNS: list[str] = [
|
||
r'EXAMPLE',
|
||
r'example',
|
||
r'YOUR_API_KEY',
|
||
r'TODO',
|
||
r'<your-',
|
||
r'placeholder',
|
||
r'xxxxxxxx',
|
||
]
|
||
|
||
# 文件名白名单:命中则跳过该文件的所有发现
|
||
WHITELIST_FILE_PATTERNS: list[str] = [
|
||
r"\.example\.",
|
||
r"^test_",
|
||
r"^mock_",
|
||
r"_test\.py$",
|
||
r"\.fixture\.",
|
||
]
|
||
|
||
# 用户确认记录(首次发现需确认,后续同模式自动放行)
|
||
USER_CONFIRMATIONS: dict[str, dict] = {} # {fingerprint: {pattern, confirmed_at, user}}
|
||
|
||
|
||
class ScanMode:
|
||
"""扫描模式:advisory(只报告,不阻止)/ blocking(阻止合并)"""
|
||
ADVISORY = "advisory"
|
||
BLOCKING = "blocking"
|
||
|
||
|
||
@dataclass
|
||
class ScanFinding:
|
||
rule: str
|
||
file: str
|
||
line: int
|
||
match: str # 截断显示,不包含完整密钥
|
||
severity: str = "high" # high | medium | low
|
||
|
||
|
||
@dataclass
|
||
class ScanReport:
|
||
task_id: str
|
||
findings: list[ScanFinding] = field(default_factory=list)
|
||
whitelisted: int = 0
|
||
clean: bool = True
|
||
advisory_blocked: bool = False
|
||
|
||
|
||
def scan_file(file_path: Path, task_id: str = "") -> ScanReport:
|
||
findings: list[ScanFinding] = []
|
||
whitelisted = 0
|
||
|
||
try:
|
||
content = file_path.read_text(encoding="utf-8", errors="replace")
|
||
except Exception:
|
||
return ScanReport(task_id=task_id, clean=True)
|
||
|
||
for rule, pattern in SECRET_PATTERNS:
|
||
for match in re.finditer(pattern, content, re.IGNORECASE):
|
||
matched_text = match.group(0)
|
||
if any(re.search(ap, matched_text) for ap in ALLOWLIST_PATTERNS):
|
||
whitelisted += 1
|
||
continue
|
||
|
||
line_no = content[:match.start()].count("\n") + 1
|
||
display = matched_text[:60] + "..." if len(matched_text) > 60 else matched_text
|
||
findings.append(ScanFinding(
|
||
rule=rule, file=str(file_path), line=line_no, match=display,
|
||
))
|
||
|
||
return ScanReport(
|
||
task_id=task_id,
|
||
findings=findings,
|
||
whitelisted=whitelisted,
|
||
clean=len(findings) == 0,
|
||
)
|
||
|
||
|
||
def scan_result_data(result: dict, task_id: str = "") -> ScanReport:
|
||
"""扫描 Worker result.json 中的敏感数据。"""
|
||
import json
|
||
text = json.dumps(result, ensure_ascii=False)
|
||
findings: list[ScanFinding] = []
|
||
whitelisted = 0
|
||
|
||
for rule, pattern in SECRET_PATTERNS:
|
||
for match in re.finditer(pattern, text, re.IGNORECASE):
|
||
matched_text = match.group(0)
|
||
if any(re.search(ap, matched_text) for ap in ALLOWLIST_PATTERNS):
|
||
whitelisted += 1
|
||
continue
|
||
display = matched_text[:60] + "..." if len(matched_text) > 60 else matched_text
|
||
findings.append(ScanFinding(
|
||
rule=rule, file="result.json", line=0, match=display,
|
||
))
|
||
|
||
return ScanReport(
|
||
task_id=task_id,
|
||
findings=findings,
|
||
whitelisted=whitelisted,
|
||
clean=len(findings) == 0,
|
||
)
|
||
|
||
|
||
def scan_file_with_mode(
|
||
file_path: Path,
|
||
task_id: str = "",
|
||
mode: str = ScanMode.BLOCKING,
|
||
confirm_callback=None, # 可选:首次发现时调用此回调询问用户
|
||
) -> ScanReport:
|
||
"""
|
||
增强版扫描:
|
||
1. 基础扫描(已有逻辑)
|
||
2. 文件名白名单过滤
|
||
3. 模式判断(advisory vs blocking)
|
||
"""
|
||
report = scan_file(file_path, task_id) # 原有逻辑
|
||
|
||
# 文件名白名单过滤
|
||
filtered_findings = []
|
||
for f in report.findings:
|
||
filename = file_path.name
|
||
if any(re.search(p, filename) for p in WHITELIST_FILE_PATTERNS):
|
||
report.whitelisted += 1
|
||
continue
|
||
filtered_findings.append(f)
|
||
|
||
report.findings = filtered_findings
|
||
report.clean = len(filtered_findings) == 0
|
||
|
||
# 模式处理
|
||
if not report.clean and mode == ScanMode.ADVISORY:
|
||
# advisory 模式:只记录,不阻止
|
||
report.advisory_blocked = False
|
||
elif not report.clean and mode == ScanMode.BLOCKING:
|
||
# blocking 模式:默认阻止
|
||
report.advisory_blocked = True
|
||
|
||
return report
|
||
|
||
|
||
def confirm_pattern(task_id: str, pattern: str, user: str = "unknown") -> None:
|
||
"""用户确认某模式为安全后,记录下来"""
|
||
from air_runtime.utils import now_iso
|
||
fingerprint = f"{task_id}:{pattern}"
|
||
USER_CONFIRMATIONS[fingerprint] = {
|
||
"pattern": pattern,
|
||
"confirmed_at": now_iso(),
|
||
"user": user,
|
||
}
|
||
|
||
|
||
def is_confirmed(task_id: str, pattern: str) -> bool:
|
||
"""检查某模式是否已被用户确认"""
|
||
fingerprint = f"{task_id}:{pattern}"
|
||
return fingerprint in USER_CONFIRMATIONS
|