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
179 changes: 172 additions & 7 deletions .github/workflows/public-repo-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ 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 five files together:
# .github/workflows/public-repo-guard.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.
Expand All @@ -25,24 +27,52 @@ name: public-repo-guard
# path glob to a repo-root `.guardignore`, or extend the repo-local `.gitleaks.toml`.

on:
# `edited` matters as much as `opened`: a body can be made to leak long after the
# PR is first raised, and until this workflow covered it, nothing ever re-scanned.
pull_request:
types: [opened, edited, reopened, synchronize]
issues:
types: [opened, edited]
issue_comment:
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
types: [created, edited]
# Reviews are the remaining public text channel: a review BODY and a line-level
# review comment are just as world-readable as an issue comment, and neither
# arrives as `issue_comment`. Without these two events a leak pasted into a
# review would bypass the body gate entirely.
pull_request_review:
types: [submitted, edited]
pull_request_review_comment:
types: [created, edited]
push:
branches: [main, master]
workflow_dispatch:

# `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.
permissions:
contents: read

concurrency:
group: public-repo-guard-${{ github.ref }}
cancel-in-progress: true
# Concurrency is per JOB, not per workflow: the two jobs want opposite behaviour.
# A workflow-level group would force one policy on both, and it showed: rapid body
# edits cancelled the tree job over and over, and every cancelled check-run stays
# attached to the commit, so the PR reported UNSTABLE while the live runs were green.

jobs:
guard:
name: Secrets + content policy
# Skips issue/comment events (the tree scan has nothing to say about a comment,
# and the org should not pay for a gitleaks run every time anyone posts one) and
# skips `edited` (a title or body edit does not change the tree).
if: >-
(github.event_name == 'pull_request' && github.event.action != 'edited')
|| github.event_name == 'push'
|| github.event_name == 'workflow_dispatch'
Comment on lines +66 to +69

@devin-ai-integration devin-ai-integration Bot Aug 6, 2026

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 is skipped on edited, which changes what the required check reports

With edited added to the pull_request types, every body/title edit now starts a workflow run in which the guard job is skipped, producing a fresh check-run with conclusion skipped on the head commit. GitHub treats skipped as passing for required checks, so this should not block merges, but it does mean the visible status for "Secrets + content policy" on a PR can end up being a skip rather than the earlier green tree scan. Worth confirming against the repo's branch-protection configuration if that check is required.

Open in Devin Review

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

GitHub treats a skipped check-run as passing for required checks, so the intentional edited skip on the tree job cannot block merges; the behavior is by design and documented in the workflow, and branch-protection configuration is not verifiable or changeable from this repo's code.

concurrency:
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

# 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 @@ -64,10 +94,145 @@ 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 run every rule through `rg -P` (PCRE2), and not every
# rg build has it: Ubuntu 22.04's apt package is built WITHOUT PCRE2, which
# would make every rule fail closed on perfectly clean input. Accept a
# preinstalled rg only if it actually has PCRE2; otherwise install the
# official release binary (which bundles PCRE2), pinned and SHA-256-verified
# the same way gitleaks is above.
- name: Install ripgrep (PCRE2-enabled, pinned + checksum-verified)
env:
RIPGREP_VERSION: "14.1.1"
RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e"
run: |
if rg --pcre2-version >/dev/null 2>&1; then 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

# The other half of a public repo's surface. `guard` above scans the published
# TREE; a PR/issue/comment/review BODY is just as world-readable and, until this
# job, was scanned by nothing server-side. That gap was real, not theoretical: a
# PR was blocked for naming a private repo in wrangler.toml while the very same
# name, with more operational detail attached, sat unchallenged in its body.
#
# Honest about what it can and cannot do. On a PR this PREVENTS the merge. On an
# issue, comment, or review 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.
body-guard:
name: Body content policy
if: >-
github.event_name == 'pull_request'
|| github.event_name == 'issues'
|| github.event_name == 'issue_comment'
|| github.event_name == 'pull_request_review'
|| github.event_name == 'pull_request_review_comment'
concurrency:
# Keyed on the specific PR / comment / 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/review IDs must come FIRST. Review events carry a
# `pull_request` object in their payload too, so a PR-number-first chain
# would coalesce every review and line comment on a PR into ONE group —
# and GitHub keeps at most one pending run per group, silently dropping
# the rest of a batch unscanned. Each distinct piece of text gets its own
# group; the PR number is only the fallback for `pull_request` events,
# where dropping an intermediate edit is fine because the newest queued
# run scans the current body.
#
# 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 and makes an otherwise-green PR look broken.
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:
# TRUST BOUNDARY: on `pull_request` the default checkout is the PR's merge
# ref, which would let a fork PR ship a neutered body-policy.sh and pass its
# own body — the PR would supply the very script that gates it. This job
# needs no PR code at all (the text under test comes from the event payload),
# so it checks out the BASE repo's default branch and always runs the
# already-reviewed copy of the scanner. A PR that changes body-policy.sh
# still has its copy exercised — by the self-test step in `guard`, against
# fixtures on the tree, where the required check reviews it.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔍 actions/checkout bumped to an unverified v7.0.1 pin, out of step with the rest of the repo

This PR changes the checkout pin from 93cb6efe… # v5.0.1 to 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 in both jobs, while every other workflow in this repo pins actions/checkout@v4 (see .github/workflows/_checks.yml:23 and the test-*.yml files pinning @v4). The bump is unrelated to the stated purpose of the PR (body scanning) and cannot be verified offline. If the SHA/tag comment do not correspond to a real published release, both jobs fail at the very first step and the whole gate goes red. Worth confirming the SHA against the upstream release, and confirming the version actually supports the sparse-checkout / sparse-checkout-cone-mode inputs used at .github/workflows/public-repo-guard.yml:181-182.

Open in Devin Review

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

with:
ref: ${{ github.event.repository.default_branch }}
# 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
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

- name: Install ripgrep (PCRE2-enabled, pinned + checksum-verified)
env:
RIPGREP_VERSION: "14.1.1"
RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e"
run: |
if rg --pcre2-version >/dev/null 2>&1; then 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-guard)::Event payload contains no pull_request/issue/comment/review object — refusing to report a pass on an unscanned body."
exit 1
fi
# `.review.body` covers pull_request_review (the summary text of a
# review); line-level review comments arrive as `.comment.body` on
# pull_request_review_comment, already extracted below.
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:
GUARD_PRIVATE_REPOS: ${{ vars.GUARD_PRIVATE_REPOS }}
run: |
# Bootstrap window: on the PR that first installs this gate, the default
# branch does not carry body-policy.sh yet, so the trusted checkout above
# cannot supply it. Only push access to the default branch can create or
# clear that state — a fork PR cannot remove the script from main — so a
# loud skip here is not a bypass. The moment the installing PR merges,
# this path is dead.
if [ ! -f scripts/public-repo-guard/body-policy.sh ]; then
echo "::warning title=public-repo-guard (body-guard)::body-policy.sh is not on the default branch yet (gate still bootstrapping) — the body scan activates once the installing PR merges."
exit 0
fi
bash scripts/public-repo-guard/body-policy.sh "$RUNNER_TEMP/bodyscan/body.txt"
Loading
Loading