GitHub Actions 安全扫描工具集:检测CVE-2025-30066漏洞与密钥泄露

8 阅读5分钟

GitHub Actions 安全扫描工具集:CVE-2025-30066 检测与防御

本工具集专为应对 CVE-2025-30066 漏洞设计,旨在帮助开发者和安全团队主动扫描 GitHub Actions 工作流中的潜在恶意行为、风险组件以及可能通过日志泄露的密钥。通过自动化扫描,您可以在攻击者利用之前发现并修复安全隐患,保障 CI/CD 流水线的安全。

功能特性

CxGithubActionsScan - 工作流恶意行为扫描

  • 多维度扫描模式:支持按组织(--org)、按单个仓库(--repo)或按特定用户(--user)进行扫描,灵活适配不同管理场景。
  • 高风险 Actions 检测:自动识别工作流文件中是否使用了已知与 CVE-2025-30066 相关的风险 Actions,如 reviewdog/*tj-actions/* 系列。
  • 恶意代码片段检测:扫描工作流文件中的 Base64 编码可疑载荷,识别潜在的隐蔽后门或信息窃取脚本。
  • 递归目录扫描:能够深入 .github/workflows 目录及其子目录,全面审查所有 YAML 配置文件。

CxGithub2msScan - 工作流日志密钥泄露扫描

  • 自动化日志收集:通过 GitHub API 批量获取指定时间范围内(如最近 7 天)的 GitHub Actions 工作流运行记录。
  • 强大的密钥检测引擎:集成 Checkmarx 2ms 工具,对下载的日志文件进行深度扫描,精准识别已泄露的密钥、令牌和密码。
  • 并发处理:利用 concurrent.futures 实现多线程扫描,大幅提升大规模日志文件的处理效率。
  • 扫描报告输出:清晰输出每个日志文件中发现的密钥数量及详细扫描结果,便于快速定位和响应。

安装指南

系统要求

  • 操作系统:Windows、Linux、macOS
  • Python 版本:3.6 或更高版本
  • GitHub 访问权限:需要具有相应权限的 GitHub 个人访问令牌

依赖安装

  1. 克隆或下载本工具集 到本地目录。

  2. 安装 Python 依赖包

    pip install requests
    
  3. 下载并配置 Checkmarx 2ms 工具(仅 CxGithub2msScan 需要):

    • 访问 Checkmarx/2ms GitHub 仓库 下载适用于您操作系统的 2ms 可执行文件。
    • 将可执行文件放置于系统 PATH 环境变量包含的目录中,或直接放置在与 CxGithub2msScan.py 相同的目录下。

配置 GitHub 个人访问令牌

  1. 登录 GitHub,点击右上角头像 → Settings
  2. 进入 Developer settingsPersonal access tokensTokens (classic)
  3. 点击 Generate new token,为令牌命名并选择以下权限范围:
    • 对于组织扫描:需要 repo(完全控制私有仓库)和 read:org(读取组织数据)。
    • 对于用户/仓库扫描:至少需要 public_repo(访问公共仓库)或 repo(访问私有仓库)。
  4. 生成并复制令牌,在运行脚本时通过 --token 参数提供。

使用说明

CxGithubActionsScan:扫描工作流中的风险

扫描一个组织内所有仓库
python CxGithubActionsScan.py --org your-organization-name --token YOUR_GITHUB_PAT
扫描单个指定仓库
python CxGithubActionsScan.py --repo owner/repo-name --token YOUR_GITHUB_PAT

或使用完整 GitHub URL:

python CxGithubActionsScan.py --repo https://github.com/owner/repo-name --token YOUR_GITHUB_PAT
扫描特定用户的所有公共仓库
python CxGithubActionsScan.py --user github-username --token YOUR_GITHUB_PAT

CxGithub2msScan:扫描工作流日志中的密钥泄露

扫描指定仓库最近 7 天的日志
python CxGithub2msScan.py --owner your-org-or-username --repo your-repo-name --days 7 --token YOUR_GITHUB_TOKEN --output ./logs
参数说明
  • --owner:仓库所属的组织名或用户名。
  • --repo:仓库名称。
  • --days:要扫描的日志天数范围(从当前时间向前推)。
  • --token:GitHub 个人访问令牌。
  • --output:下载的日志文件存储目录。
核心代码

CxGithubActionsScan - 核心扫描逻辑 (片段)

# 递归扫描仓库中的指定路径,检测风险 Actions 和恶意代码
def scan_path(owner, repo, path, search_terms):
    url = f"https://api.github.com/repos/{owner}/{repo}/contents/{path}"
    response = requests.get(url, headers=HEADERS)
    if response.status_code == 404:
        return {}
    if response.status_code != 200:
        print(f"Error retrieving {path} in {owner}/{repo}: {response.status_code}")
        return {}

    data = response.json()
    results = {}

    if isinstance(data, dict):
        # 处理单个文件(Base64 编码)
        if "content" in data and data.get("encoding") == "base64":
            try:
                content = base64.b64decode(data["content"]).decode("utf-8")
            except Exception as e:
                print(f"Error decoding content in {owner}/{repo}/{path}: {e}")
                return results
            # 检查文件中是否包含任何风险特征词
            matches = [term for term in search_terms if term in content]
            if matches:
                results[path] = matches
        return results
    # ... 处理目录的逻辑

CxGithub2msScan - 日志扫描核心逻辑 (片段)

# 获取指定时间范围内的所有工作流运行记录
def get_workflow_runs(owner, repo, days, token):
    headers = {"Accept": "application/vnd.github.v3+json", "Authorization": f"Bearer {token}"}
    runs = []
    page = 1
    per_page = 100
    cutoff = datetime.datetime.utcnow() - datetime.timedelta(days=days)
    while True:
        url = f"https://api.github.com/repos/{owner}/{repo}/actions/runs"
        params = {"per_page": per_page, "page": page}
        resp = requests.get(url, headers=headers, params=params)
        if resp.status_code != 200:
            break
        data = resp.json()
        if "workflow_runs" not in data or not data["workflow_runs"]:
            break
        for run in data["workflow_runs"]:
            created_at = datetime.datetime.strptime(run["created_at"], "%Y-%m-%dT%H:%M:%SZ")
            if created_at < cutoff:
                return runs  # 返回截止时间之前的 runs
            runs.append(run)
        page += 1
    return runs

# 使用 2ms 引擎扫描单个文件
def scan_file(file_path):
    print(f"Running Checkmarx 2ms on {file_path}...")
    try:
        result = subprocess.run(["2ms.exe", "filesystem", "--path", file_path],
                                capture_output=True, text=True)
        if result.returncode != 0:
            return {"file": file_path, "error": result.stderr.strip()}
        # 从输出中提取找到的密钥数量
        match = re.search(r"totalsecretsfound:\s*(\d+)", result.stdout, re.IGNORECASE)
        secret_count = int(match.group(1)) if match else 0
        if secret_count > 0:
            return {"file": file_path, "secrets_found": secret_count, "output": result.stdout.strip()}
        else:
            return {"file": file_path, "secrets_found": 0}
    except Exception as e:
        return {"file": file_path, "error": str(e)}
```FINISHED
6HFtX5dABrKlqXeO5PUv/7b4YVDDMwcrE+FmfPoZGK2bxM2ff+ga26S+bYmqNFP+wkH49J3l6n8JiqJi3LVqOw==