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
136 changes: 136 additions & 0 deletions .github/workflows/public-repo-guard-body.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
name: public-repo-guard-body

# The other half of public-repo-guard.yml's coverage, deliberately in its OWN
# workflow file — see the long comment block at the top of public-repo-guard.yml
# for the incident (wave-av/cli PR 68) that caused the split and why it is a
# file-level split, not just a job-level one.
#
# `guard` (in public-repo-guard.yml) scans the published TREE and produces the
# REQUIRED check "Secrets + content policy". This job scans a PR/issue/comment
# BODY, which is just as world-readable and, until this job existed, was scanned
# by nothing server-side. That gap was real, not theoretical: a PR was blocked
# for naming a private repo in a config file while the very same name, with more
# operational detail attached, sat unchallenged in its body.
#
# This job's check-run name ("Body content policy") is NOT a required status
# context in this repo's ruleset, so it can safely trigger on every comment/review
# event without any risk of masking or wedging the required tree-scan context —
# that is the entire reason it lives in a separate file from the tree scan.
#
# Honest about what it can and cannot do. On a PR this PREVENTS the merge. On an
Comment on lines +15 to +20

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: This check is explicitly not required, so a failing body scan does not prevent a PR merge despite the workflow claiming that it does. [api mismatch]

Assessment: 🔴 Critical · 🔁 Occurrence: Sometimes

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** .github/workflows/public-repo-guard-body.yml
**Line:** 15:20
**Comment:**
	*Api Mismatch: This check is explicitly not required, so a failing body scan does not prevent a PR merge despite the workflow claiming that it does.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

# issue or comment the text is already public the moment it posts, so this is
# detection — it tells us to go redact, fast. Only the client-side pre-write hook
# can stop that class before publication.
#
# Accepted side effect of that detection: on issue/comment events github.sha is
# the default-branch head, so a leak in an issue or comment surfaces as a red
# check on main. That is intentional (a leak SHOULD be loud somewhere a human
# looks) and safe for merges because the branch-protection-required check is
# `Secrets + content policy`, not this one. Keep it that way: making this name
# required would let any commenter turn main's checks red at will.
on:
# `edited` matters as much as `opened`: a body can be made to leak long after
# the PR is first raised, and until this job covered it, nothing re-scanned it.
pull_request:
types: [opened, edited, reopened, synchronize]
issues:
types: [opened, edited]
issue_comment:
types: [created, edited]
# Inline review comments on a diff are a SEPARATE event from issue_comment —
# without this trigger they are world-readable text that no job ever scans.
pull_request_review_comment:
types: [created, edited]
# A submitted review's top-level body (the free-text field above any inline
# comments) is yet another world-readable payload, separate from BOTH comment
# events — without this trigger nothing ever scans it.
pull_request_review:
types: [submitted, edited]

# `pull_request`, deliberately NOT `pull_request_target`: a fork PR must never get
# a write token or repo secrets just because a gate wanted to read its body. That
# is also why this job can run the repo's own copy of the scanner directly — with
# no token and no secrets in the job, PR-supplied code has nothing to steal, and
# the only thing a PR can do by editing the scanner is fail its own check.
permissions:
contents: read

jobs:
body-guard:
name: Body content policy
concurrency:
# Keyed on the specific comment / review / PR / issue rather than github.ref,
# because issue events all report the default branch and a ref-keyed group
# would let two comments cancel each other, leaving one unscanned. The comment
# and review ids come FIRST: those payloads also carry the PR number, and
# keying them on the PR would collapse two rapid comments into one group,
# dropping a verdict.
#
# cancel-in-progress is deliberately FALSE. Every version of a body deserves a
# verdict, the job is seconds long, and a cancelled check-run lingers on the
# commit. Since this check-run name is not required, a lingering cancelled
# run here cannot wedge a merge the way the tree scan's could — but a dropped
# verdict on a body would still be a real coverage gap, so the same "let it
# finish" policy applies.
group: public-repo-guard-body-${{ github.event.comment.id || github.event.review.id || github.event.pull_request.number || github.event.issue.number || github.ref }}
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# Only the gate's own scripts are needed — no reason to pay for the whole
# tree on every comment.
sparse-checkout: scripts/public-repo-guard
sparse-checkout-cone-mode: false
# This job only reads the scripts — never leave the token sitting in
# .git/config while repo-supplied scripts execute in the workspace.
persist-credentials: false

# Same rationale as the tree job: body-policy.sh needs a PCRE2-enabled rg,
# and Ubuntu's apt package has none.
- name: Install ripgrep (pinned + checksum-verified, PCRE2 build)
env:
RIPGREP_VERSION: "14.1.1"
RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e"
run: |
if command -v rg >/dev/null && rg --pcre2-version >/dev/null 2>&1; then
echo "using preinstalled $(rg --version | head -n1) with PCRE2"; exit 0
fi
curl -fsSL --proto '=https' --tlsv1.2 -o ripgrep.tar.gz \
"https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz"
echo "${RIPGREP_SHA256} ripgrep.tar.gz" | sha256sum -c -
tar -xzf ripgrep.tar.gz --strip-components=1 "ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl/rg"
sudo install -m 0755 rg /usr/local/bin/rg
rm -f rg ripgrep.tar.gz
rg --pcre2-version

# The body is read straight out of the event payload FILE and written to
# another file. It is never interpolated into a run: block and never placed
# in an environment variable, so shell metacharacters in a hostile PR body
# have nothing to act on. jq is preinstalled on the GitHub-hosted images.
- name: Materialize the untrusted title/body to a file
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/bodyscan"
# An UNRECOGNIZED payload shape must fail, never quietly scan nothing and
# report a pass. If the event schema ever moves, this job must go red
# rather than become a green rubber stamp over an unscanned body.
if [ "$(jq -r 'has("pull_request") or has("issue") or has("comment") or has("review")' "$GITHUB_EVENT_PATH")" != "true" ]; then
echo "::error title=public-repo-guard-body::Event payload contains no pull_request/issue/comment/review object — refusing to report a pass on an unscanned body."
exit 1
fi
jq -r '[.pull_request.title, .pull_request.body,
.issue.title, .issue.body,
.comment.body, .review.body]
| map(select(. != null)) | join("\n")' \
"$GITHUB_EVENT_PATH" > "$RUNNER_TEMP/bodyscan/body.txt"
echo "scanning $(wc -l < "$RUNNER_TEMP/bodyscan/body.txt") line(s) of body text"

- name: body policy (PR / issue / comment text)
env:
# body-policy.sh FAILS CLOSED (exit 2) if this is empty in CI: an unset,
# misspelled, or unexposed variable must surface as a red check, never as
# a silent skip of the private-repo rule. A repo with deliberately nothing
# to guard sets the variable to the literal 'none'.
GUARD_PRIVATE_REPOS: ${{ vars.GUARD_PRIVATE_REPOS }}
run: bash scripts/public-repo-guard/body-policy.sh "$RUNNER_TEMP/bodyscan/body.txt"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The PR event checks out PR-controlled files, then runs the scanner from that checkout, allowing an author to modify the scanner and bypass body-policy enforcement. [security]

Assessment: 🔴 Critical · 🔁 Occurrence: Often

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** .github/workflows/public-repo-guard-body.yml
**Line:** 136:136
**Comment:**
	*Security: The PR event checks out PR-controlled files, then runs the scanner from that checkout, allowing an author to modify the scanner and bypass body-policy enforcement.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

108 changes: 98 additions & 10 deletions .github/workflows/public-repo-guard.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: public-repo-guard

# Pre-publication content gate for WAVE public repos. Two complementary checks:
# Pre-publication content gate for WAVE public repos. Two complementary checks,
# split across TWO workflow files (this one, plus public-repo-guard-body.yml):
# 1. gitleaks — formatted secrets (API keys, tokens, private keys) in the tree.
# 2. content-policy.sh — WAVE-specific leaks gitleaks misses: live Stripe account
# IDs, hardcoded Cloudflare account_ids, developer absolute paths, references
Expand All @@ -13,19 +14,65 @@ name: public-repo-guard
# wave-av/.github must not be able to alter another repo's secret scanner). The
# gitleaks binary is version-pinned AND SHA-256-verified before it runs.
#
# To install on a new repo, copy all three files together:
# To install on a new repo, copy all six files together (the guard job RUNS the
# body-policy fixtures as a step, so the test file is part of the install unit,
# not an optional extra):
# .github/workflows/public-repo-guard.yml
# .github/workflows/public-repo-guard-body.yml
# .gitleaks.toml
# scripts/public-repo-guard/content-policy.sh
# scripts/public-repo-guard/body-policy.sh
# scripts/public-repo-guard/tests/body-policy.test.sh
#
# Scan scope: the published working TREE (gitleaks --no-git), NOT git history. The
# goal is "what is public right now is clean", so a shallow checkout is sufficient.
#
# Allowlisting: annotate a verified-safe line with `# guard:allow <reason>`, add a
# path glob to a repo-root `.guardignore`, or extend the repo-local `.gitleaks.toml`.

#
# WHY THIS IS A SEPARATE WORKFLOW FROM public-repo-guard-body.yml (an earlier draft
# of this change put both jobs in this one file — see the incident below):
#
# The `guard` job below produces the check-run named "Secrets + content policy",
# which is the REQUIRED status context in this repo's branch-protection ruleset.
# Two failure modes are possible for any job that produces a required check-run
# from a shared, comment/review-triggered `on:` block, and BOTH were hit in
# production before this split:
#
# (a) MASKING: if this job's `on:` trigger set includes the comment/review events
# the BODY scan needs (the tree scan does not) and the job then uses a
# job-level `if:` to skip those events (because the tree hasn't changed),
# GitHub still publishes a check-run named "Secrets + content policy" with
# conclusion `skipped` on that event. Branch protection treats `skipped` as
# passing, and the newest check-run for a name wins — so a bare comment could
# flip an already-FAILED required tree scan green with nothing re-examining
# the tree.
#
# (b) CHURN / FALSE BLOCK: making the job run for real on every comment event,
# combined with `cancel-in-progress: true` (needed so a genuine new commit
# supersedes a stale scan promptly), means a burst of review-bot comments —
# which do not change the tree at all — repeatedly re-fires and cancels the
# SAME job. Every cancelled run leaves a `cancelled` check-run attached to
# the commit under the required name. Observed live in wave-av/cli PR 68:
# four review-bot comments within 68s produced seven check-runs named
# "Secrets + content policy" (five cancelled, two success); the checks tab
# showed the latest as green, but the commit's status-check rollup reported
# FAILURE and the PR was permanently MERGEABLE/BLOCKED even though the gate
# had genuinely passed.
#
# (a) and (b) are the SAME structural problem: this job's required check-run name
# was reachable from an `on:` trigger set that also had to serve comment/review
# events for the (unrelated) body scan. Splitting into two workflow FILES — not
# just two jobs — removes the shared trigger set entirely: this file's `on:` block
# now lists ONLY events that can change the tree. A review comment or a title/body
# edit never matches this workflow's trigger at all, so GitHub never runs it and
# never publishes ANY check-run — skipped, cancelled, or otherwise — under the
# required name for that event. There is nothing left to mask and nothing left to
# cancel. Coverage is unchanged: every event that can actually alter the published
# tree still gets a real, non-skippable gitleaks + content-policy run.
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches: [main, master]
workflow_dispatch:
Expand All @@ -34,19 +81,35 @@ on:
# never reports on the queue's temporary ref and every queued PR waits forever.
merge_group:

# `pull_request`, deliberately NOT `pull_request_target`: a fork PR must never get
# a write token or repo secrets just because a gate wanted to read the tree.
permissions:
contents: read

concurrency:
group: public-repo-guard-${{ github.ref }}
cancel-in-progress: true

jobs:
guard:
name: Secrets + content policy
# No job-level `if:` — deliberately. The `on:` block above already scopes this
# job to exactly the tree-changing events, so every triggering event is a real
# run: never skipped, never a candidate for the masking bug described above,
# and never skipped out from under the merge queue.
concurrency:
# cancel-in-progress is TRUE here and it is now safe: the only rapid
# retrigger this workflow can see is `synchronize` (a new commit landing
# while a prior scan of the OLD tree is still running), and superseding a
# stale in-flight scan with a fresh one for the new tree is exactly the
# right behaviour. Comment/review bursts cannot reach this workflow at all
# (see the block comment above), so this can no longer produce the
# cancelled-run pileup that wedged wave-av/cli PR 68.
group: public-repo-guard-tree-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
with:
# This job only reads the tree — never leave the token sitting in
# .git/config while repo-supplied scripts execute in the workspace.
persist-credentials: false

# gitleaks' GitHub Action requires a paid license for organizations; the CLI
# itself is MIT-licensed and free. Pin the version AND verify the release
Expand All @@ -68,10 +131,35 @@ jobs:
- name: gitleaks (secret scan — published tree)
run: gitleaks detect --no-git --source . --config .gitleaks.toml --redact --no-banner --exit-code 1

- name: Install ripgrep
run: command -v rg >/dev/null || (sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep)
# Both policy scripts use `rg -P` (PCRE2). Ubuntu's apt ripgrep is built
# WITHOUT PCRE2, so `rg -P` exits 2 there and the scripts fail closed —
# red on every run, which gets a gate switched off. Accept a preinstalled
# rg only if it actually has PCRE2; otherwise install the official release
# binary (PCRE2 compiled in), pinned and checksum-verified like gitleaks.
- name: Install ripgrep (pinned + checksum-verified, PCRE2 build)
env:
RIPGREP_VERSION: "14.1.1"
RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e"
run: |
if command -v rg >/dev/null && rg --pcre2-version >/dev/null 2>&1; then
echo "using preinstalled $(rg --version | head -n1) with PCRE2"; exit 0
fi
curl -fsSL --proto '=https' --tlsv1.2 -o ripgrep.tar.gz \
"https://github.com/BurntSushi/ripgrep/releases/download/${RIPGREP_VERSION}/ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl.tar.gz"
echo "${RIPGREP_SHA256} ripgrep.tar.gz" | sha256sum -c -
tar -xzf ripgrep.tar.gz --strip-components=1 "ripgrep-${RIPGREP_VERSION}-x86_64-unknown-linux-musl/rg"
sudo install -m 0755 rg /usr/local/bin/rg
rm -f rg ripgrep.tar.gz
rg --pcre2-version

- name: content policy (WAVE trade-secret / internal-leak gate)
env:
GUARD_PRIVATE_REPOS: ${{ vars.GUARD_PRIVATE_REPOS }}
run: bash scripts/public-repo-guard/content-policy.sh .

# The body gate's own fixtures. Its negatives are the load-bearing half — a
# leak gate that blocks legitimate cross-repo references gets switched off,
# and then it protects nothing. Runs here so a regression is caught by CI
# rather than by a leak.
- name: body policy self-test (fixtures)
run: bash scripts/public-repo-guard/tests/body-policy.test.sh
Comment on lines +164 to +165

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟨 Tree-scan job executes the pull request's own guard scripts as part of a required check

The required guard job checks out the PR's tree and runs scripts/public-repo-guard/tests/body-policy.test.sh (and, pre-existing, content-policy.sh) straight from that untrusted checkout, so a PR can rewrite the fixtures or the policy script it is being judged by and make the self-test pass unconditionally. The new body-guard job explicitly avoids exactly this (.github/workflows/public-repo-guard.yml:164-192 resolves the scanner from the trusted base/default ref), so the two halves of the same gate apply opposite trust models.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Loading
Loading