Skip to content

Commit a451ff2

Browse files
committed
feat: add installer security advisory
1 parent df99423 commit a451ff2

3 files changed

Lines changed: 104 additions & 0 deletions

File tree

__tests__/e2e/install-script.e2e.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ describe('review-router curl installer e2e', () => {
122122

123123
expect(result.status).toBe(0);
124124
expect(fs.existsSync(result.workflowPath)).toBe(true);
125+
expect(result.stdout).toContain('Security advisory');
126+
expect(result.stdout).toContain(
127+
'Generated workflows use pull_request, skip fork PR secret-backed review, and do not use pull_request_target.'
128+
);
129+
expect(result.stdout).toContain(
130+
'Skipping remote repository hardening checks in dry-run/local-only mode'
131+
);
125132
expect(result.stdout).toContain('ReviewRouter doctor');
126133
expect(result.stdout).toContain('Workflow files are present');
127134
expect(result.stdout).toContain(
@@ -260,6 +267,9 @@ describe('review-router curl installer e2e', () => {
260267

261268
expect(result.status).toBe(0);
262269
expect(result.stdout).toContain('ReviewRouter doctor');
270+
expect(result.stdout).toContain(
271+
'Codex OAuth stores your ChatGPT-managed Codex auth.json as an Actions secret'
272+
);
263273
expect(result.stdout).toContain('Skipping CODEX_CONFIG_TOML by default');
264274
expect(result.stdout).not.toContain('gh secret set CODEX_CONFIG_TOML');
265275
const workflow = workflowText(result.workflowPath);

docs/install.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,25 @@ REVIEW_ROUTER_SKIP_APP_DOCTOR=1
134134

135135
Do not use this for production rollout. Without `Actions: write`, `/rr skip` can record the signed ledger but cannot rerun the failed review check automatically.
136136

137+
## Security advisory
138+
139+
The installer prints a security advisory before writing workflows.
140+
141+
It always reminds you that generated workflows:
142+
143+
- use `pull_request`, not `pull_request_target`
144+
- skip fork PR secret-backed review by default
145+
- keep `/rr skip` handling in a separate workflow
146+
147+
When GitHub API access is available, it also checks:
148+
149+
- whether the target repository is public while using Codex OAuth
150+
- whether the default branch has branch protection
151+
- whether a CODEOWNERS file exists
152+
- whether CODEOWNERS appears to cover `.github/workflows/**`
153+
154+
These checks are warnings, not hard blockers. The main risk is not that collaborators can read Actions secrets in the GitHub UI. The practical risk is workflow-code access: anyone who can get a malicious workflow merged into a repository or push a same-repository branch where secrets are available can try to exfiltrate secrets during CI.
155+
137156
## Secret scopes
138157

139158
| Scope | Where secrets live | Repository access | Best for |
@@ -259,6 +278,8 @@ ReviewRouter uses a binary-only Codex health check by default. The real review c
259278

260279
Use this only in trusted automation. Do not put personal Codex OAuth credentials into public/open-source repos where untrusted workflow changes can access secrets. GitHub does not expose repository secrets to fork PR workflows by default, and the generated workflow skips fork PRs by default.
261280

281+
Important auth freshness note: on GitHub-hosted runners, the runner filesystem is ephemeral. Codex can refresh `auth.json` during a run, but ReviewRouter cannot safely write the refreshed file back to GitHub Actions secrets automatically. If the stored secret becomes stale or a refresh token is rotated, run `codex login` again on a trusted machine and rerun the installer or update `CODEX_AUTH_JSON`. For fully automatic long-running auth, prefer an OpenAI API key or a trusted self-hosted runner with persistent `CODEX_HOME`.
282+
262283
Default Codex model:
263284

264285
```text

scripts/install.sh

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -530,6 +530,78 @@ can_run_remote_checks() {
530530
! is_true "$DRY_RUN" && ! is_true "$LOCAL_ONLY" && ! is_true "$SKIP_GH_CHECK"
531531
}
532532

533+
decode_base64_to_stdout() {
534+
if base64 --decode >/dev/null 2>&1 </dev/null; then
535+
base64 --decode
536+
else
537+
base64 -D
538+
fi
539+
}
540+
541+
read_github_file() {
542+
repo_path="$1"
543+
ref="$2"
544+
encoded="$(gh api "repos/$TARGET_REPO/contents/$repo_path?ref=$ref" --jq '.content' 2>/dev/null || true)"
545+
[ -n "$encoded" ] || return 1
546+
printf '%s' "$encoded" | tr -d '\n' | decode_base64_to_stdout 2>/dev/null
547+
}
548+
549+
find_codeowners_content() {
550+
ref="$1"
551+
for codeowners_path in CODEOWNERS .github/CODEOWNERS docs/CODEOWNERS; do
552+
if content="$(read_github_file "$codeowners_path" "$ref")"; then
553+
printf '%s\n' "$content"
554+
return 0
555+
fi
556+
done
557+
return 1
558+
}
559+
560+
run_security_advisory() {
561+
log ""
562+
info "Security advisory"
563+
if [ "$AUTH_MODE" = "codex" ]; then
564+
warn "Codex OAuth stores your ChatGPT-managed Codex auth.json as an Actions secret. Use it only for trusted private automation; prefer OpenAI API key mode for public/open-source repositories."
565+
warn "GitHub-hosted runners are ephemeral. If Codex refreshes auth.json during a run, ReviewRouter cannot persist the refreshed file back to GitHub secrets automatically; reseed auth.json if Codex starts returning 401."
566+
else
567+
ok "Auth mode does not store Codex OAuth auth.json"
568+
fi
569+
ok "Generated workflows use pull_request, skip fork PR secret-backed review, and do not use pull_request_target."
570+
571+
if ! can_run_remote_checks; then
572+
warn "Skipping remote repository hardening checks in dry-run/local-only mode"
573+
return
574+
fi
575+
576+
repo_visibility="$(gh repo view "$TARGET_REPO" --json visibility --jq '.visibility' 2>/dev/null || true)"
577+
default_branch="$(gh repo view "$TARGET_REPO" --json defaultBranchRef --jq '.defaultBranchRef.name' 2>/dev/null || true)"
578+
[ -n "$default_branch" ] || default_branch="main"
579+
580+
if [ "$AUTH_MODE" = "codex" ] && [ "$repo_visibility" = "PUBLIC" ]; then
581+
warn "Target repository is public and auth mode is Codex OAuth. GitHub does not pass Actions secrets to fork PR workflows, but Codex OAuth is still not recommended for public/open-source repos."
582+
fi
583+
584+
if gh api "repos/$TARGET_REPO/branches/$default_branch/protection" >/dev/null 2>&1; then
585+
ok "Default branch protection is enabled on $default_branch"
586+
if [ "$(gh api "repos/$TARGET_REPO/branches/$default_branch/protection" --jq '.required_pull_request_reviews != null' 2>/dev/null || printf 'false')" != "true" ]; then
587+
warn "Default branch protection exists, but required pull request reviews were not detected. Require reviews for workflow changes before relying on secret-backed review."
588+
fi
589+
else
590+
warn "Default branch protection was not detected on $default_branch. Protect the branch before storing Codex OAuth or provider API secrets."
591+
fi
592+
593+
if codeowners="$(find_codeowners_content "$default_branch")"; then
594+
ok "CODEOWNERS file detected"
595+
if printf '%s\n' "$codeowners" | grep -Eq '(^|[[:space:]])\.github/(\*\*|workflows(/|\*\*)?)'; then
596+
ok "CODEOWNERS appears to cover .github workflows"
597+
else
598+
warn "CODEOWNERS exists, but .github/workflows/** ownership was not detected. Add a workflow owners rule for stronger secret protection."
599+
fi
600+
else
601+
warn "CODEOWNERS was not detected. Add CODEOWNERS for .github/workflows/** so workflow changes require trusted reviewers."
602+
fi
603+
}
604+
533605
verify_codex_auth_file() {
534606
auth_file="$1"
535607
[ -f "$auth_file" ] || fatal "Codex auth file not found: $auth_file. Run: codex login"
@@ -1644,6 +1716,7 @@ main() {
16441716
info "Preset: $PRESET"
16451717
info "Action ref: $ACTION_REF"
16461718

1719+
run_security_advisory
16471720
setup_identity
16481721
setup_auth
16491722
setup_ledger_secret

0 commit comments

Comments
 (0)