Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,23 @@
不回显可能包含 secret 名称的审计输出。
- 审计输出首行回显基线版本与受保护分支。

### 可观测性

- `[WORKFLOW_LIST]` 的"path 不合法"分支改为回显 workflow name 与 path。此前只输出
一句无主语的错误,维护者无法区分"项目把 workflow 放错位置"与"GitHub 平台内建的
`dynamic/` workflow 不在仓库里",也就无从判断该改项目还是改基线。
- 平台内建跳过列表抽出为 `SKIPPED_PLATFORM_WORKFLOWS`;未知 `dynamic/` 路径保持
fail-closed。

### 修复(第二轮 live 审计发现)

- **`[REPOSITORY_MERGE_POLICY]` 的假阳性。** 原判定是
`metadata.get("allow_auto_merge") is not False`,字段缺失时 `.get()` 返回 `None`,
`None is not False` 成立,于是报告"仓库必须关闭 Auto-merge"。而 GitHub 只在认证
身份具备 admin 权限时才返回这组合并策略字段,只读审计 App 很可能拿不到。结果是
四个仓库连续数周被误报,维护者被指引去关一个本来就关着的开关。现在区分三种情况:
`true` 报漂移、`false` 通过、缺失报 `[API_UNAVAILABLE]` 并指向 App 权限。

### 测试

- 新增 `tests/unit/`:对表达式解析器、secret 静态可枚举性、`gh api` 重试与分支
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
审计只验证可由结构化 YAML 与仓库配置可靠判断的高层约束,逐条 MUST 见[公共契约](contracts/ci-cd-baseline.md):

- active PR/release workflow 的事件、权限、runner、Environment 与 secret 边界;
- 受控 release 之外发现任一发布能力即 fail-closed;GitHub 内建的 `dynamic/dependabot/update-graph` 明确跳过,其他 active workflow 读不到即 fail-closed;
- 受控 release 之外发现任一发布能力即 fail-closed;仅 `SKIPPED_PLATFORM_WORKFLOWS` 中明确列出的 GitHub 内建 workflow 会跳过,其他 `dynamic/` 路径与读不到的 workflow 一律 fail-closed,错误信息回显 name 与 path 供定位;
- 外部 Action 的 40 位 SHA、版本注释、`docker://` 内容摘要,以及 workflow 与本地 Action 闭包中的全部 `uses:` 引用;
- `.github/release-safety.yml` 的 schema、语义 job/step 引用、依赖、condition 映射与步骤全序,以及中央独立验证的 publish/deploy/finalize 公共语义;
- 项目发布安全契约 wrapper 是 mode `100755` 的普通文件,且被唯一的 `PR container validation` job 以单行 `run` 精确调用;
Expand Down
6 changes: 3 additions & 3 deletions contracts/error-codes.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@
|---|---|
| `[ARGUMENT]` | 仓库参数不是 `owner/repo`,或 `BASELINE_DEFAULT_BRANCH` 不是合法分支名 |
| `[API]` | GitHub 响应结构不符合预期,属于可判定的异常 |
| `[API_UNAVAILABLE]` | 重试后仍无法读取 GitHub:网络、5xx/429 或响应不是合法 JSON |
| `[API_UNAVAILABLE]` | 重试后仍无法读取 GitHub:网络、5xx/429、响应不是合法 JSON,或 admin 门控字段(如 `allow_auto_merge`)未返回导致无法判定 |

## 仓库与分支

| 错误码 | 含义 |
|---|---|
| `[DEFAULT_BRANCH]` | 仓库 default_branch 不等于基线受保护分支 |
| `[REPOSITORY_MERGE_POLICY]` | 官方 Review 尚非 required check,仓库必须关闭 Auto-merge |
| `[REPOSITORY_MERGE_POLICY]` | `allow_auto_merge` 显式为 `true`。字段缺失表示无法判定,走 `[API_UNAVAILABLE]`,不算漂移 |
| `[BRANCH_PROTECTION]` | strict、PR review、管理员保护、对话解决、force push 或删除保护缺失 |
| `[REQUIRED_CHECK]` | 受保护分支未要求 `PR container validation` |
| `[REQUIRED_CHECK_APP]` | required check 未绑定 GitHub Actions app,或 checks 结构无效 |
Expand All @@ -41,7 +41,7 @@

| 错误码 | 含义 |
|---|---|
| `[WORKFLOW_LIST]` | active workflow 缺少合法 path,或无法从受保护分支读取 |
| `[WORKFLOW_LIST]` | active workflow 的 path 不在 `.github/workflows/` 下(错误信息回显 name 与 path),或无法从受保护分支读取 |
| `[WORKFLOW_STRUCTURE]` | workflow 或本地 Action 的 YAML 结构不满足最低约束 |
| `[ACTION_TREE]` | recursive tree 被截断或条目结构无效,拒绝不完整审计 |

Expand Down
32 changes: 28 additions & 4 deletions scripts/audit_github_baseline.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@
"docker/login-action": ("dbcb813823bdd20940b903addbd779551569679f", "v4.6.0"),
}
ALLOWED_AUX_PERMISSIONS = {"contents": "read", "security-events": "write"}
# GitHub 平台内建、不存在于仓库中的 workflow:无法读取内容,也无法被项目删除。
# 只跳过明确已知的路径,未知的 dynamic/ 路径仍然 fail-closed。
SKIPPED_PLATFORM_WORKFLOWS = frozenset({"dynamic/dependabot/update-graph"})
HTTP_STATUS_PATTERN = re.compile(r"\(HTTP (\d{3})\)")
RETRYABLE_HTTP_STATUSES = frozenset({408, 429, 500, 502, 503, 504})
GH_API_MAX_ATTEMPTS = 4
Expand Down Expand Up @@ -83,6 +86,9 @@ class RepositorySource:
local_actions: dict[str, ParsedYaml] = field(default_factory=dict)
release_manifest: ParsedYaml | None = None
errors: list[str] = field(default_factory=list)
# 判定不了的项:不是漂移,但会让本仓结论不可信。收集而不是抛出,
# 这样其余检查照常执行,维护者一次就能看到全部问题。
unavailable: list[str] = field(default_factory=list)


@dataclass(frozen=True)
Expand Down Expand Up @@ -593,20 +599,37 @@ def build_source(repo: str) -> RepositorySource:
branch = default_branch()
if metadata.get("default_branch") != branch:
source.errors.append(f"[DEFAULT_BRANCH] 仓库 default_branch 必须是 {branch}")
if metadata.get("allow_auto_merge") is not False:
# GitHub 只在认证身份具备 admin 权限时返回 allow_auto_merge 这组合并策略字段。
# 字段缺失说明"看不到",不等于"开着";把两者混为一谈会让维护者去关一个本来就
# 关着的开关,而真正该修的是令牌权限。
auto_merge = metadata.get("allow_auto_merge")
if auto_merge is True:
source.errors.append(
"[REPOSITORY_MERGE_POLICY] 官方 Review 尚非 required check,仓库必须关闭 Auto-merge"
)
elif auto_merge is not False:
source.unavailable.append(
"[API_UNAVAILABLE] 无法判定 Auto-merge:GET repos/"
f"{repo} 未返回 allow_auto_merge(实际值 {auto_merge!r})。"
"该字段需要 admin 级读取权限,请确认审计 App 的 Administration 权限已授予该仓库"
)

workflows = gh_api_paginated(repo, "actions/workflows", "workflows")
for workflow in workflows:
if workflow.get("state") != "active":
continue
path = workflow.get("path")
if path == "dynamic/dependabot/update-graph":
if path in SKIPPED_PLATFORM_WORKFLOWS:
continue
if not isinstance(path, str) or not path.startswith(".github/workflows/"):
source.errors.append("[WORKFLOW_LIST] active workflow 缺少合法 path")
# 必须回显 name/path:否则维护者拿到这条错误无法判断是哪个 workflow,
# 也无法区分"项目放错位置"与"GitHub 平台内建的 dynamic/ workflow"。
name = workflow.get("name")
identity = f"{name!r} " if isinstance(name, str) else ""
source.errors.append(
f"[WORKFLOW_LIST] active workflow {identity}的 path 不在 "
f".github/workflows/ 下:{path!r}"
)
continue
try:
text = repository_content(repo, path)
Expand Down Expand Up @@ -2193,7 +2216,8 @@ def audit_repository(repo: str) -> tuple[list[str], bool]:
"[REPO_SECRET_BOUNDARY] repository scope 仍有 secret 名称:"
+ ", ".join(sorted(repository_secret_names))
)
return errors, False
# 判定不了的项排在最后,并使本仓结论整体不可信;其余检查已照常执行完毕。
return errors + source.unavailable, bool(source.unavailable)
except AuditUnavailable as error:
return [f"[API_UNAVAILABLE] {error}"], True
except AuditError as error:
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/default-branch-main/repo.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"full_name": "example/project", "default_branch": "main"}
{"full_name": "example/project", "default_branch": "main", "allow_auto_merge": false}
8 changes: 8 additions & 0 deletions tests/fixtures/dynamic-unknown-workflow/workflows.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"total_count": 3,
"workflows": [
{"name": "PR validation", "path": ".github/workflows/pr-ci.yml", "state": "active"},
{"name": "Master release", "path": ".github/workflows/release.yml", "state": "active"},
{"name": "CodeQL", "path": "dynamic/github-code-scanning/codeql", "state": "active"}
]
}
1 change: 1 addition & 0 deletions tests/fixtures/repository-auto-merge-unknown/repo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"full_name": "example/project", "default_branch": "master"}
7 changes: 7 additions & 0 deletions tests/test-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,8 @@ PY

run_expect_success good
run_expect_failure repository-auto-merge-enabled "[REPOSITORY_MERGE_POLICY]"
# 字段缺失是"看不到"而非"开着":必须报审计未完成,不能伪装成漂移。
run_expect_unavailable repository-auto-merge-unknown
run_expect_failure code-review-rules-missing "[CODE_REVIEW_RULES]"
run_expect_failure code-review-rules-symlink "[CODE_REVIEW_RULES]"
run_expect_failure code-review-rules-heading-missing "[CODE_REVIEW_RULES]"
Expand Down Expand Up @@ -730,6 +732,11 @@ run_expect_failure release-dynamic-secret "[SECRET_ENV_BOUNDARY]"
run_expect_failure release-secrets-inherit "[SECRET_ENV_BOUNDARY]"
run_expect_success dynamic-platform-workflow
run_expect_failure stale-active-workflow "[WORKFLOW_LIST]"
# 未知的 dynamic/ workflow 仍然 fail-closed,但必须回显 name 与 path 供定位。
run_expect_failure_codes dynamic-unknown-workflow \
"[WORKFLOW_LIST]" \
"'CodeQL'" \
"'dynamic/github-code-scanning/codeql'"
run_expect_failure auxiliary-missing-permissions "[AUX_PR_PERMISSION]"
check_expression_unit_tests
check_templates
Expand Down