feat: 一键部署沙箱 + Docker 镜像发布工作流

- docker-compose: 移除沙箱 profiles 配置,支持一键 docker compose up -d
- pyproject.toml: 迁移 dev-dependencies 到 dependency-groups (PEP 735)
- 新增 docker-publish.yml 工作流,支持手动发布镜像(不创建 tag)
- 优化 orchestrator 和 verification agent

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
lintsinghua 2025-12-16 20:00:00 +08:00
parent f71b8da7df
commit cd79242b3e
5 changed files with 179 additions and 17 deletions

129
.github/workflows/docker-publish.yml vendored Normal file
View File

@ -0,0 +1,129 @@
name: Docker Publish
# 只构建并推送 Docker 镜像,不创建 Release 或 Tag
on:
workflow_dispatch:
inputs:
tag:
description: '镜像标签 (例如: latest, dev, v3.0.0)'
required: true
default: 'latest'
type: string
build_frontend:
description: '构建前端镜像'
required: false
type: boolean
default: true
build_backend:
description: '构建后端镜像'
required: false
type: boolean
default: true
build_sandbox:
description: '构建沙箱镜像'
required: false
type: boolean
default: true
jobs:
build-and-push:
name: 构建并推送镜像
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: 检出代码
uses: actions/checkout@v4
- name: 设置 Node.js
if: ${{ github.event.inputs.build_frontend == 'true' }}
uses: actions/setup-node@v4
with:
node-version: '20'
- name: 安装 pnpm
if: ${{ github.event.inputs.build_frontend == 'true' }}
uses: pnpm/action-setup@v4
with:
version: 9
- name: 安装前端依赖
if: ${{ github.event.inputs.build_frontend == 'true' }}
working-directory: ./frontend
run: pnpm install --frozen-lockfile
- name: 构建前端项目
if: ${{ github.event.inputs.build_frontend == 'true' }}
working-directory: ./frontend
run: pnpm build
env:
VITE_USE_LOCAL_DB: 'true'
- name: 登录到 GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: 设置 QEMU
uses: docker/setup-qemu-action@v3
- name: 设置 Docker Buildx
uses: docker/setup-buildx-action@v3
- name: 构建并推送前端 Docker 镜像
if: ${{ github.event.inputs.build_frontend == 'true' }}
uses: docker/build-push-action@v5
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository_owner }}/deepaudit-frontend:${{ github.event.inputs.tag }}
cache-from: type=gha,scope=frontend
cache-to: type=gha,mode=max,scope=frontend
- name: 构建并推送后端 Docker 镜像
if: ${{ github.event.inputs.build_backend == 'true' }}
uses: docker/build-push-action@v5
with:
context: ./backend
file: ./backend/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository_owner }}/deepaudit-backend:${{ github.event.inputs.tag }}
cache-from: type=gha,scope=backend
cache-to: type=gha,mode=max,scope=backend
- name: 构建并推送沙箱 Docker 镜像
if: ${{ github.event.inputs.build_sandbox == 'true' }}
uses: docker/build-push-action@v5
with:
context: ./docker/sandbox
file: ./docker/sandbox/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
ghcr.io/${{ github.repository_owner }}/deepaudit-sandbox:${{ github.event.inputs.tag }}
cache-from: type=gha,scope=sandbox
cache-to: type=gha,mode=max,scope=sandbox
- name: 输出镜像信息
run: |
echo "## 镜像已推送到 GHCR" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ github.event.inputs.build_frontend }}" == "true" ]; then
echo "- \`ghcr.io/${{ github.repository_owner }}/deepaudit-frontend:${{ github.event.inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ github.event.inputs.build_backend }}" == "true" ]; then
echo "- \`ghcr.io/${{ github.repository_owner }}/deepaudit-backend:${{ github.event.inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ github.event.inputs.build_sandbox }}" == "true" ]; then
echo "- \`ghcr.io/${{ github.repository_owner }}/deepaudit-sandbox:${{ github.event.inputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
fi

View File

@ -893,17 +893,32 @@ Action Input: {{"参数": "值"}}
if same_file and (same_line or similar_desc or same_type):
# Update existing with new info (e.g. verification results)
# Prefer verified data over unverified
merged = {**existing_f, **normalized_new}
# 🔥 FIX: Smart merge - don't overwrite good data with empty values
merged = dict(existing_f) # Start with existing data
for key, value in normalized_new.items():
# Only overwrite if new value is meaningful
if value is not None and value != "" and value != 0:
merged[key] = value
elif key not in merged or merged[key] is None:
# Fill in missing fields even with empty values
merged[key] = value
# Keep the better title
if normalized_new.get("title") and len(normalized_new.get("title", "")) > len(existing_f.get("title", "")):
merged["title"] = normalized_new["title"]
# Keep verified status if either is verified
if existing_f.get("is_verified") or normalized_new.get("is_verified"):
merged["is_verified"] = True
# 🔥 FIX: Preserve non-zero line numbers
if existing_f.get("line_start") and not normalized_new.get("line_start"):
merged["line_start"] = existing_f["line_start"]
# 🔥 FIX: Preserve vulnerability_type
if existing_f.get("vulnerability_type") and not normalized_new.get("vulnerability_type"):
merged["vulnerability_type"] = existing_f["vulnerability_type"]
self._all_findings[i] = merged
found = True
logger.info(f"[Orchestrator] Merged finding: {new_file}:{new_line} ({new_type})")
logger.info(f"[Orchestrator] Merged finding: {new_file}:{merged.get('line_start', 0)} ({merged.get('vulnerability_type', '')})")
break
if not found:

View File

@ -679,13 +679,32 @@ class VerificationAgent(BaseAgent):
final_result = None
if final_result and "findings" in final_result:
# 🔥 DEBUG: Log what LLM returned for verdict diagnosis
verdicts_debug = [(f.get("file_path", "?"), f.get("verdict"), f.get("confidence")) for f in final_result["findings"]]
logger.info(f"[{self.name}] LLM returned verdicts: {verdicts_debug}")
for f in final_result["findings"]:
# 🔥 FIX: Normalize verdict - handle missing/empty verdict
verdict = f.get("verdict")
if not verdict or verdict not in ["confirmed", "likely", "uncertain", "false_positive"]:
# Try to infer verdict from other fields
if f.get("is_verified") is True:
verdict = "confirmed"
elif f.get("confidence", 0) >= 0.8:
verdict = "likely"
elif f.get("confidence", 0) <= 0.3:
verdict = "false_positive"
else:
verdict = "uncertain"
logger.warning(f"[{self.name}] Missing/invalid verdict for {f.get('file_path', '?')}, inferred as: {verdict}")
verified = {
**f,
"is_verified": f.get("verdict") == "confirmed" or (
f.get("verdict") == "likely" and f.get("confidence", 0) >= 0.8
"verdict": verdict, # 🔥 Ensure verdict is set
"is_verified": verdict == "confirmed" or (
verdict == "likely" and f.get("confidence", 0) >= 0.8
),
"verified_at": datetime.now(timezone.utc).isoformat() if f.get("verdict") in ["confirmed", "likely"] else None,
"verified_at": datetime.now(timezone.utc).isoformat() if verdict in ["confirmed", "likely"] else None,
}
# 添加修复建议

View File

@ -202,10 +202,10 @@ exclude_lines = [
"if TYPE_CHECKING:",
]
# ============ UV Configuration ============
# ============ Dependency Groups (PEP 735) ============
[tool.uv]
dev-dependencies = [
[dependency-groups]
dev = [
"pytest>=7.4.0",
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.1.0",

View File

@ -111,14 +111,13 @@ services:
- deepaudit-network
# 沙箱镜像构建服务 (漏洞验证必须)
# 注意: 此服务仅用于构建镜像,不会持续运行
# 注意: 此服务仅用于构建镜像,构建完成后自动退出
sandbox:
build:
context: ./docker/sandbox
dockerfile: Dockerfile
image: deepaudit/sandbox:latest
profiles:
- build-only
restart: "no"
command: echo "Sandbox image built successfully"
networks: