Skip to content

Conversation

@tusik
Copy link

@tusik tusik commented Dec 24, 2025

User description

在原来git仓库token认证的基础上增加:

  • 新增SSH密钥管理功能,包括生成、查看、测试和删除SSH密钥对。
  • 在agent_tasks.py中集成SSH私钥解密和SSH克隆逻辑,支持git@格式的SSH URL。
  • 在projects.py中为SSH URL添加文件获取支持。
  • 新增ssh_keys.py端点提供完整的SSH密钥API管理。前端Account页面新增SSH密钥管理界面,Projects页面支持选择SSH Key认证类型。
  • 新增git_ssh_service.py提供SSH密钥生成、验证和Git SSH操作功能。
image image

PR Type

Enhancement


Description

  • Add comprehensive SSH key authentication support for Git repositories

    • Generate, store, and manage RSA 4096 SSH key pairs
    • Encrypt private keys and store in user configuration
    • Support git@ format SSH URLs for repository access
  • Integrate SSH operations into agent tasks and project file retrieval

    • Detect SSH URLs and route to GitSSHOperations for cloning
    • Support SSH authentication in repository scanning workflow
    • Fallback to HTTPS when SSH not configured
  • Implement SSH key management API endpoints and frontend UI

    • Generate, retrieve, delete, and test SSH keys via REST API
    • Add SSH key management section in Account page
    • Update Projects page to support SSH Key authentication type
  • Add SSH connection testing and validation features

    • Verify SSH key pair matching and public key fingerprints
    • Test SSH connectivity to Git services with detailed error messages

Diagram Walkthrough

flowchart LR
  User["User"] -->|Generate SSH Key| SSHKeyAPI["SSH Keys API<br/>ssh_keys.py"]
  SSHKeyAPI -->|Encrypt & Store| UserConfig["User Config<br/>Database"]
  User -->|Configure Project| Projects["Projects Page"]
  Projects -->|Select SSH Key Type| AgentTasks["Agent Tasks<br/>agent_tasks.py"]
  AgentTasks -->|Detect SSH URL| GitSSH["GitSSHOperations<br/>git_ssh_service.py"]
  GitSSH -->|Clone with SSH| Repository["Git Repository<br/>GitHub/GitLab"]
  User -->|Test SSH Key| SSHKeyAPI -->|Verify Connection| Repository
  Scanner["Scanner Service<br/>scanner.py"] -->|SSH URL| GitSSH
Loading

File Walkthrough

Relevant files
Configuration changes
2 files
api.py
Register SSH keys API router                                                         
+2/-1     
serverClient.ts
Configure HTTP client redirect handling                                   
+2/-0     
Enhancement
8 files
ssh_keys.py
New SSH key management API endpoints                                         
+227/-0 
git_ssh_service.py
SSH key generation and Git operations service                       
+478/-0 
agent_tasks.py
Integrate SSH authentication into agent task execution     
+144/-55
projects.py
Add SSH URL support for project file retrieval                     
+44/-16 
scanner.py
Support SSH authentication in repository scanning               
+83/-48 
sshKeys.ts
New SSH keys API client module                                                     
+61/-0   
Account.tsx
Add SSH key management UI to Account page                               
+265/-1 
Projects.tsx
Update project form to support SSH Key authentication       
+41/-11 

新增SSH密钥管理功能,包括生成、查看、测试和删除SSH密钥对。在agent_tasks.py中集成SSH私钥解密和SSH克隆逻辑,支持git@格式的SSH URL。在projects.py中为SSH URL添加文件获取支持。新增ssh_keys.py端点提供完整的SSH密钥API管理。前端Account页面新增SSH密钥管理界面,Projects页面支持选择SSH Key认证类型。新增git_ssh_service.py提供SSH密钥生成、验证和Git SSH操作功能。
@vercel
Copy link

vercel bot commented Dec 24, 2025

@tusik is attempting to deploy a commit to the tsinghuaiiilove-2257's projects Team on Vercel.

A member of the Team first needs to authorize it.

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Dec 24, 2025

PR Compliance Guide 🔍

Below is a summary of compliance checks for this PR:

Security Compliance
🔴
Private key exposure

Description: SSH private keys are written to temporary files with world-readable permissions before
set_secure_file_permissions is called, creating a race condition window where sensitive
key material could be accessed by other processes or users on the system.
git_ssh_service.py [228-232]

Referred Code
# 写入私钥
with open(key_file, 'w') as f:
    f.write(private_key)
set_secure_file_permissions(key_file)
MITM attack vulnerability

Description: The SSH clone operation uses StrictHostKeyChecking=no and UserKnownHostsFile=/dev/null,
disabling host key verification and enabling man-in-the-middle attacks where attackers can
intercept Git repository clones and inject malicious code.
agent_tasks.py [2520-2549]

Referred Code
# SSH URL使用GitSSHOperations(支持SSH密钥认证)
if is_ssh_url and ssh_private_key:
    async def run_ssh_clone():
        return await asyncio.to_thread(
            GitSSHOperations.clone_repo_with_ssh,
            repo_url, ssh_private_key, base_path, branch
        )

    clone_task = asyncio.create_task(run_ssh_clone())
    while not clone_task.done():
        check_cancelled()
        try:
            result = await asyncio.wait_for(asyncio.shield(clone_task), timeout=1.0)
            break
        except asyncio.TimeoutError:
            continue

    if clone_task.done():
        result = clone_task.result()

    # GitSSHOperations返回字典格式


 ... (clipped 9 lines)
SSH host verification disabled

Description: The SSH connection test uses StrictHostKeyChecking=no and UserKnownHostsFile=/dev/null,
disabling host key verification and making the system vulnerable to man-in-the-middle
attacks during SSH key testing.
git_ssh_service.py [391-402]

Referred Code
# 构建SSH命令(只使用DeepAudit密钥)
cmd = [
    'ssh',
    '-i', key_file,
    '-o', 'StrictHostKeyChecking=no',
    '-o', 'UserKnownHostsFile=/dev/null',
    '-o', 'ConnectTimeout=10',
    '-o', 'PreferredAuthentications=publickey',
    '-o', 'IdentitiesOnly=yes',  # 只使用指定的密钥,不使用系统默认密钥
    '-v',  # 详细输出
    '-T', f'git@{host_part}'
]
Command injection risk

Description: The set_secure_file_permissions function uses subprocess.run with user-controlled
environment variables (os.environ.get("USERNAME")) in Windows icacls commands without
validation, enabling potential command injection if the USERNAME environment variable is
maliciously crafted.
git_ssh_service.py [19-50]

Referred Code
def set_secure_file_permissions(file_path: str):
    """
    设置文件的安全权限(Unix: 0600, Windows: 只有当前用户可读写)

    Args:
        file_path: 文件路径
    """
    if sys.platform == 'win32':
        # Windows系统使用icacls命令设置权限
        try:
            # 移除所有继承的权限
            subprocess.run(
                ['icacls', file_path, '/inheritance:r'],
                capture_output=True,
                check=True
            )
            # 只给当前用户完全控制权限
            subprocess.run(
                ['icacls', file_path, '/grant:r', f'{os.environ.get("USERNAME")}:(F)'],
                capture_output=True,
                check=True


 ... (clipped 11 lines)
Resource exhaustion vulnerability

Description: The SSH key generation endpoint lacks rate limiting, allowing attackers to exhaust system
resources by repeatedly generating RSA 4096 keys, which are computationally expensive
operations.
ssh_keys.py [44-96]

Referred Code
@router.post("/generate", response_model=SSHKeyGenerateResponse)
async def generate_ssh_key(
    *,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(deps.get_current_user),
) -> Any:
    """
    生成新的SSH密钥对

    生成RSA 4096格式的SSH密钥对,私钥加密存储在用户配置中,公钥返回给用户
    """
    try:
        # 生成SSH密钥对 (RSA 4096)
        private_key, public_key = SSHKeyService.generate_rsa_key(key_size=4096)

        # 获取或创建用户配置
        result = await db.execute(
            select(UserConfig).where(UserConfig.user_id == current_user.id)
        )
        user_config = result.scalar_one_or_none()



 ... (clipped 32 lines)
Repository bomb DoS

Description: The get_repo_files_via_ssh function clones entire repositories to temporary directories
and walks the filesystem without size limits or timeouts, enabling denial-of-service
attacks through repository bombs (extremely large or deeply nested repositories).
git_ssh_service.py [296-353]

Referred Code
"""
temp_clone_dir = None
try:
    # 创建临时克隆目录
    temp_clone_dir = tempfile.mkdtemp(prefix='deepaudit_clone_')

    # 克隆仓库
    clone_result = GitSSHOperations.clone_repo_with_ssh(
        repo_url, private_key, temp_clone_dir, branch
    )

    if not clone_result['success']:
        raise Exception(f"克隆仓库失败: {clone_result.get('error', '')}")

    # 扫描目录获取文件列表
    from app.services.scanner import is_text_file, should_exclude

    files = []
    for root, dirs, filenames in os.walk(temp_clone_dir):
        # 排除.git目录
        if '.git' in dirs:


 ... (clipped 37 lines)
Ticket Compliance
🎫 No ticket provided
  • Create ticket/issue
Codebase Duplication Compliance
Codebase context is not defined

Follow the guide to enable codebase context checks.

Custom Compliance
🟢
Generic: Meaningful Naming and Self-Documenting Code

Objective: Ensure all identifiers clearly express their purpose and intent, making code
self-documenting

Status: Passed

Learn more about managing compliance generic rules or creating your own custom rules

🔴
Generic: Comprehensive Audit Trails

Objective: To create a detailed and reliable record of critical system actions for security analysis
and compliance.

Status:
Missing audit logging: Critical SSH key operations (generate, delete, test) lack audit logging with user ID,
timestamp, and action outcomes.

Referred Code
@router.post("/generate", response_model=SSHKeyGenerateResponse)
async def generate_ssh_key(
    *,
    db: AsyncSession = Depends(get_db),
    current_user: User = Depends(deps.get_current_user),
) -> Any:
    """
    生成新的SSH密钥对

    生成RSA 4096格式的SSH密钥对,私钥加密存储在用户配置中,公钥返回给用户
    """
    try:
        # 生成SSH密钥对 (RSA 4096)
        private_key, public_key = SSHKeyService.generate_rsa_key(key_size=4096)

        # 获取或创建用户配置
        result = await db.execute(
            select(UserConfig).where(UserConfig.user_id == current_user.id)
        )
        user_config = result.scalar_one_or_none()



 ... (clipped 163 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Robust Error Handling and Edge Case Management

Objective: Ensure comprehensive error handling that provides meaningful context and graceful
degradation

Status:
Silent permission failure: File permission setting failures are caught but only print warnings without proper logging
or alerting administrators of security risks.

Referred Code
if sys.platform == 'win32':
    # Windows系统使用icacls命令设置权限
    try:
        # 移除所有继承的权限
        subprocess.run(
            ['icacls', file_path, '/inheritance:r'],
            capture_output=True,
            check=True
        )
        # 只给当前用户完全控制权限
        subprocess.run(
            ['icacls', file_path, '/grant:r', f'{os.environ.get("USERNAME")}:(F)'],
            capture_output=True,
            check=True
        )
    except Exception as e:
        print(f"Warning: Failed to set Windows file permissions: {e}")
        # 尝试使用os.chmod作为后备方案
        try:
            os.chmod(file_path, 0o600)
        except:


 ... (clipped 4 lines)

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Error Handling

Objective: To prevent the leakage of sensitive system information through error messages while
providing sufficient detail for internal debugging.

Status:
Detailed error exposure: Error messages expose internal implementation details like str(e) directly to users in API
responses without sanitization.

Referred Code
except Exception as e:
    raise HTTPException(status_code=500, detail=f"生成SSH密钥失败: {str(e)}")

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Secure Logging Practices

Objective: To ensure logs are useful for debugging and auditing without exposing sensitive
information like PII, PHI, or cardholder data.

Status:
Sensitive key logging: SSH private key decryption success is logged which could indicate presence of sensitive
credentials in system logs.

Referred Code
    logger.info("成功解密SSH私钥")
except Exception as e:

Learn more about managing compliance generic rules or creating your own custom rules

Generic: Security-First Input Validation and Data Handling

Objective: Ensure all data inputs are validated, sanitized, and handled securely to prevent
vulnerabilities

Status:
Missing URL validation: Repository URLs are not validated before use in SSH operations, potentially allowing
command injection through malicious URLs.

Referred Code
def clone_repo_with_ssh(repo_url: str, private_key: str, target_dir: str, branch: str = "main") -> Dict[str, any]:
    """
    使用SSH密钥克隆Git仓库

    Args:
        repo_url: SSH格式的Git URL (例如: git@github.com:user/repo.git)
        private_key: SSH私钥内容
        target_dir: 目标目录
        branch: 分支名称

    Returns:
        操作结果字典
    """
    temp_dir = None
    try:
        # 创建临时目录存放SSH密钥
        temp_dir = tempfile.mkdtemp(prefix='deepaudit_ssh_')
        key_file = os.path.join(temp_dir, 'id_rsa')

        # 写入私钥
        with open(key_file, 'w') as f:


 ... (clipped 50 lines)

Learn more about managing compliance generic rules or creating your own custom rules

  • Update
Compliance status legend 🟢 - Fully Compliant
🟡 - Partial Compliant
🔴 - Not Compliant
⚪ - Requires Further Human Verification
🏷️ - Compliance label

@qodo-free-for-open-source-projects
Copy link

qodo-free-for-open-source-projects bot commented Dec 24, 2025

PR Code Suggestions ✨

Explore these optional code suggestions:

CategorySuggestion                                                                                                                                    Impact
High-level
Refactor Git operations to avoid inefficiency and duplication

Refactor the Git operations to eliminate duplicated clone logic in
agent_tasks.py and replace the inefficient file-listing method in
git_ssh_service.py. The current approach of cloning a repository to list its
files is not scalable; a more efficient method like git archive --remote should
be considered.

Examples:

backend/app/api/v1/endpoints/agent_tasks.py [2520-2581]
                    # SSH URL使用GitSSHOperations(支持SSH密钥认证)
                    if is_ssh_url and ssh_private_key:
                        async def run_ssh_clone():
                            return await asyncio.to_thread(
                                GitSSHOperations.clone_repo_with_ssh,
                                repo_url, ssh_private_key, base_path, branch
                            )

                        clone_task = asyncio.create_task(run_ssh_clone())
                        while not clone_task.done():

 ... (clipped 52 lines)
backend/app/services/git_ssh_service.py [283-354]
    def get_repo_files_via_ssh(repo_url: str, private_key: str, branch: str = "main",
                                exclude_patterns: List[str] = None) -> List[Dict[str, str]]:
        """
        通过SSH克隆仓库并获取文件列表

        Args:
            repo_url: SSH格式的Git URL
            private_key: SSH私钥
            branch: 分支名称
            exclude_patterns: 排除模式列表

 ... (clipped 62 lines)

Solution Walkthrough:

Before:

# In agent_tasks.py
async def _get_project_root(...):
  # ...
  for branch in branches_to_try:
    if is_ssh_url and ssh_private_key:
      # SSH clone logic with cancellation check loop
      result = GitSSHOperations.clone_repo_with_ssh(...)
      # ...
    else:
      # HTTPS clone logic with identical cancellation check loop
      result = subprocess.run(["git", "clone", ...])
      # ...
  # ... similar duplicated logic for default branch ...

# In git_ssh_service.py
def get_repo_files_via_ssh(...):
  # Clones the entire repo to a temporary directory
  clone_result = GitSSHOperations.clone_repo_with_ssh(...)
  # Walks the local directory to list and read files
  for root, dirs, filenames in os.walk(temp_clone_dir):
    # ... read file content ...
  return files

After:

# In agent_tasks.py
async def _get_project_root(...):
  # ...
  async def unified_clone(branch):
      if is_ssh_url and ssh_private_key:
          return GitSSHOperations.clone_repo_with_ssh(..., branch=branch)
      else:
          return subprocess.run(["git", "clone", ..., "--branch", branch, ...])

  for branch in branches_to_try:
    # Single, non-duplicated clone logic with cancellation check
    result = await run_cancellable_task(unified_clone(branch))
    # ...
  # ... similar unified logic for default branch ...

# In git_ssh_service.py
def get_repo_files_via_ssh(...):
  # Use a more efficient method that doesn't require a full clone
  # For example, using `git archive --remote`
  command = f"git archive --remote={repo_url} {branch}"
  # Execute command and process the streamed archive (e.g., tar)
  # to get file paths and content without writing to disk.
  ...
  return files
Suggestion importance[1-10]: 9

__

Why: The suggestion correctly identifies significant code duplication in agent_tasks.py and a major performance bottleneck in git_ssh_service.py where a full repository clone is used just to list files, which is not scalable.

High
  • Update

@tusik
Copy link
Author

tusik commented Dec 24, 2025

使用部署密钥(Deploy key)是很常见的一种CI/CD中获取代码的方式,可以更好的进行集成

@lintsinghua
Copy link
Owner

收到

@lintsinghua
Copy link
Owner

修复一下冲突

@lintsinghua
Copy link
Owner

代码审查 - SSH 相关问题汇总

🔴 严重安全问题

1. 禁用 SSH Host Key 验证 (git_ssh_service.py:241-242)

'-o', 'StrictHostKeyChecking=no',
'-o', 'UserKnownHostsFile=/dev/null',
风险: 这使系统容易受到中间人攻击 (MITM)。攻击者可以冒充 GitHub/GitLab 服务器。
建议:
# 使用预定义的已知主机
KNOWN_HOSTS = {
'github.com': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl',
'gitlab.com': 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAfuCHKVTjquxvt6CM6tdG4SLp1Btn/nOeHHE5UOzRdf',
}

或至少对已知服务使用 known_hosts


⚠️ 中等问题

2. clone_repo_with_ssh 使用空分支名可能失败 (git_ssh_service.py:251)

cmd = ['git', 'clone', '--depth', '1', '--branch', branch, repo_url, target_dir]
问题: 当 branch="" 时(默认分支场景),--branch "" 会导致 git 命令失败。
建议:
cmd = ['git', 'clone', '--depth', '1']

if branch: # 只有明确指定分支时才添加
cmd.extend(['--branch', branch])
cmd.extend([repo_url, target_dir])

3. agent_tasks.py 中调用默认分支克隆时传空字符串 (line 2559)

GitSSHOperations.clone_repo_with_ssh,
repo_url, ssh_private_key, base_path, "" # 空字符串表示使用默认分支
问题: 这与上面的问题相关,会导致克隆失败。

4. SSH URL 检测不完整 (git_ssh_service.py:207)

return url.startswith('git@') or url.startswith('ssh://')
问题: agent_tasks.py:2320 只检查 git@:
is_ssh_url = repo_url.startswith('git@')
建议: 应该统一使用 GitSSHOperations.is_ssh_url()。

5. 私钥解密失败只是警告,可能导致后续静默失败 (agent_tasks.py:301-304)

except Exception as e:
logger.warning(f"解密SSH私钥失败: {e}")
问题: 如果用户期望使用 SSH 但解密失败,应该给出更明确的提示。

💡 建议改进

6. RSA 4096 可以考虑提供 Ed25519 选项

说明: Ed25519 更快、更安全,密钥更短。代码已有 generate_ed25519_key() 但未使用。

7. 测试连接超时可能过短 (git_ssh_service.py:408)

timeout=15
说明: 某些网络环境下 15 秒可能不够。

8. 前端 "other" 类型含义变更

<SelectItem value="other">SSH Key</SelectItem>
建议: 将 "other" 重新定义为 SSH Key 可能与现有数据不兼容。建议添加新的类型 ssh。

📋 代码质量问题

位置 问题
git_ssh_service.py:210 类型标注 Dict[str, any] 应为 Dict[str, Any]
git_ssh_service.py:284 缺少空行分隔
ssh_keys.py:91 返回值包含 fingerprint 但 schema 未定义

…e/git_ssh

# Conflicts:
#	backend/app/services/scanner.py
#	backend/uv.lock
#	frontend/src/pages/Projects.tsx
🔧 refactor(git_ssh_service):重构私钥加载逻辑,增加格式兼容性处理
@tusik
Copy link
Author

tusik commented Dec 25, 2025

冲突已解决

@lintsinghua
Copy link
Owner

问题 严重程度 状态
禁用 SSH Host Key 验证 (StrictHostKeyChecking=no) 🔴 高 ❌ 未修复
空分支名导致 git clone 失败 ⚠️ ❌ 未修复
SSH URL 检测方法不统一 ⚠️ ❌ 未修复

✨ feat(git):改进SSH URL检测和分支克隆逻辑
📝 docs(frontend):更新SSH公钥添加说明,移除CodeUp链接
@lintsinghua
Copy link
Owner

你自己测试过了吗?

新引入的 Bug:

git_ssh_service.py:273-276 有逻辑错误:

  cmd = ['git', 'clone', '--depth', '1']
  if branch:  # 只有明确指定分支时才添加
      cmd.extend(['--branch', branch])
      cmd.extend([repo_url, target_dir])  #  错误:这行在 if 块内

当 branch=None 时,repo_url 和 target_dir 不会被添加到命令,导致 git clone 失败。

正确代码应该是:

cmd = ['git', 'clone', '--depth', '1']
if branch:
    cmd.extend(['--branch', branch])
cmd.extend([repo_url, target_dir])  # 应该在 if 块外面

@lintsinghua
Copy link
Owner

有分支冲突

- 新增SSH配置目录设置,支持持久化存储known_hosts文件
- 实现known_hosts文件清理API端点,解决主机密钥变更导致的连接问题
- 优化SSH连接策略,使用StrictHostKeyChecking=accept-new自动接受新主机密钥
- 前端添加known_hosts清理按钮,提升SSH密钥管理体验
- 改进SSH测试逻辑,正确处理部署密钥的Anonymous响应
…e/git_ssh

# Conflicts:
#	backend/app/api/v1/endpoints/agent_tasks.py
@tusik
Copy link
Author

tusik commented Dec 26, 2025

增加了clone 时的know_hosts检查
增加了know_hosts清理
以及修复以上问题

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants