-
Notifications
You must be signed in to change notification settings - Fork 0
ci: scan issue and comment bodies — this repo has never scanned one #29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
d18e204
c2b1eaa
f51aeb5
4406ee3
3f60773
066920e
6e05711
f4b6d0e
6c14407
4a64de0
8a8c43b
d2e4139
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| name: public-repo-guard-body | ||
|
|
||
| # The other half of a public repo's surface. public-repo-guard.yml 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. | ||
| # | ||
| # A SEPARATE workflow file on purpose, not a second job next to the tree gate. | ||
| # The tree gate's `Secrets + content policy` check is required by the org ruleset, | ||
| # and a job-level `if` skip still creates a check run (conclusion `skipped`) on | ||
| # the PR head SHA. Required checks treat `skipped` as satisfied and the newest | ||
| # check run of a name wins, so if the tree gate's workflow also fired on | ||
| # issue/comment/review events (only to skip the tree job), any comment or review | ||
| # on a PR would launder an earlier genuine tree-scan FAILURE on the same commit | ||
| # into a mergeable PR. Keeping these triggers in their own file means the tree | ||
| # gate never emits a skipped verdict. Copy BOTH workflow files together; the full | ||
| # six-file install manifest lives in public-repo-guard.yml's header. | ||
| # | ||
| # Honest about what it can and cannot do. On a PR this check gates the merge | ||
| # only once the org ruleset requires the `Body content policy` context; the | ||
| # ruleset today requires only `Secrets + content policy` (the tree gate), so | ||
| # until that rollout lands a failing body scan is advisory on PRs too. On an | ||
| # issue or comment the text is already public the moment it posts, so this is | ||
| # detection either way: it tells us to go redact, fast. Only the client-side | ||
| # pre-write hook can stop that class before publication. | ||
|
|
||
| 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. | ||
| # `synchronize` keeps a `Body content policy` verdict on every head SHA, so the | ||
| # check is already present on each commit if the ruleset rollout makes it required. | ||
| pull_request: | ||
| types: [opened, edited, reopened, synchronize] | ||
| issues: | ||
| types: [opened, edited] | ||
| issue_comment: | ||
| types: [created, edited] | ||
| # A review's summary and its inline comments are the SAME public surface as an | ||
| # issue comment, but GitHub emits them on their own events — without these two, | ||
| # text posted in a review would never reach body-guard. | ||
| pull_request_review: | ||
| types: [submitted, edited] | ||
| pull_request_review_comment: | ||
| types: [created, 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. | ||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| # No job-level `if`: every event in the trigger list above gets a real scan, so | ||
| # this job never reports a skipped conclusion over an unscanned body. | ||
| 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/review id comes FIRST, before the PR number: review events also | ||
| # carry a top-level `pull_request` object, so a PR-number-first key put a | ||
| # batch review's N inline comments (N near-simultaneous events, each with | ||
| # DIFFERENT text) into one group, and GitHub keeps only ONE pending run per | ||
| # group: queueing a new run cancels the previously pending one, permanently | ||
| # dropping those bodies unscanned. Per-id keying isolates each comment and | ||
| # review; only re-edits of the SAME body (PR/issue title+body) share a group, | ||
| # where the surviving run scans the newest version. | ||
| # | ||
| # 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: | ||
| - 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 | ||
| # The gate only reads the tree; no later step may find the workflow token | ||
| # in .git/config — matches every other workflow in this repo. | ||
| persist-credentials: false | ||
|
Comment on lines
+79
to
+87
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟨 Body-leak gate executes the pull request's own copy of the scanner, so a PR can weaken the check that scans it On Was this helpful? React with 👍 or 👎 to provide feedback. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On pull_request / pull_request_review / pull_request_review_comment events GitHub executes the workflow YAML itself from the PR merge ref, so a PR that can neuter body-policy.sh can equally rewrite the job to emit a green check with no scan; pinning the script checkout to the default branch adds no integrity boundary. On issues/issue_comment events both the workflow and the checkout already come from the default branch, so the scanner is already trusted there. |
||
|
|
||
| # The policy script scans with `rg -P`, which needs a PCRE2-enabled build. | ||
| # Neither the runner image nor Ubuntu's apt package guarantees one, and a | ||
| # PCRE2-less rg makes every rule fail closed — so install the pinned upstream | ||
| # musl release (built with PCRE2), checksum-verified like gitleaks in the tree | ||
| # gate, and verify PCRE2 at install time, where the failure names the problem. | ||
| - name: Install ripgrep (pinned + checksum-verified, PCRE2 required) | ||
| env: | ||
| RIPGREP_VERSION: "14.1.1" | ||
| RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e" | ||
| run: | | ||
| 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 >/dev/null \ | ||
| || { echo "::error title=public-repo-guard::this ripgrep build lacks PCRE2 (required for the -P policy rules) — install a PCRE2-enabled rg."; exit 1; } | ||
|
|
||
| # 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 | ||
| jq -r '[.pull_request.title, .pull_request.body, | ||
| .issue.title, .issue.body, | ||
| .review.body, | ||
| .comment.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 / review text) | ||
| env: | ||
| GUARD_PRIVATE_REPOS: ${{ vars.GUARD_PRIVATE_REPOS }} | ||
| run: bash scripts/public-repo-guard/body-policy.sh "$RUNNER_TEMP/bodyscan/body.txt" | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,10 +13,15 @@ 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: | ||
| # .github/workflows/public-repo-guard.yml | ||
| # .github/workflows/public-repo-guard-body.yml (the body gate; a SEPARATE file | ||
| # on purpose, see that file's header for why it must not live here) | ||
| # .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 (run unconditionally as a | ||
| # self-test step below — omit it and the gate fails on every push) | ||
| # | ||
| # 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. | ||
|
|
@@ -25,24 +30,49 @@ name: public-repo-guard | |
| # path glob to a repo-root `.guardignore`, or extend the repo-local `.gitleaks.toml`. | ||
|
|
||
| on: | ||
| # This file's trigger list must contain ONLY events on which `guard` performs a | ||
| # real scan. The job is the required `Secrets + content policy` check, and a | ||
| # job-level `if` skip still creates a check run (conclusion `skipped`) on the | ||
| # PR head SHA. Required checks treat `skipped` as satisfied and the newest check | ||
| # run of a name wins, so one skipped run would launder an earlier genuine FAILURE | ||
| # on the same commit into a mergeable PR. That is why the body gate, with its | ||
| # issue/comment/review triggers, lives in public-repo-guard-body.yml instead of | ||
| # here, and why `edited` below runs a full scan rather than being filtered out: | ||
| # a title edit re-scans an unchanged merge ref (same verdict, small cost), and a | ||
| # base-branch retarget (which also arrives as `edited`, with no `synchronize` | ||
| # guaranteed to follow) re-scans the NEW merge ref it actually changes. | ||
| pull_request: | ||
| types: [opened, edited, reopened, synchronize] | ||
| 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 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`: every event this workflow triggers on gets a real scan. | ||
| # See the trigger comment above: a skipped run of a required check is not | ||
| # neutral, it REPLACES a failing verdict on the same head SHA. | ||
| concurrency: | ||
| group: public-repo-guard-tree-${{ github.event.pull_request.number || github.ref }} | ||
| # `edited` fires on every title/body edit (bots edit PR bodies constantly) and | ||
| # re-scans an UNCHANGED merge ref — so it must not cancel an in-flight run on | ||
| # the same head SHA, or every body edit leaves a cancelled check-run attached | ||
| # to the commit. `synchronize` (and push) DO supersede the previous merge ref, | ||
| # so those still cancel the now-stale scan. | ||
| cancel-in-progress: ${{ github.event.action != 'edited' }} | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: The new body leak gate can be bypassed on pull requests because it executes Prompt for AI agents |
||
| with: | ||
| # The gate only reads the tree; no later step may find the workflow token | ||
| # in .git/config — matches every other workflow in this repo. | ||
| 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 | ||
|
|
@@ -64,10 +94,33 @@ 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 scan with `rg -P`, which needs a PCRE2-enabled build. | ||
| # Neither the runner image nor Ubuntu's apt package guarantees one, and a | ||
| # PCRE2-less rg makes every rule fail closed — so install the pinned upstream | ||
| # musl release (built with PCRE2), checksum-verified like gitleaks above, and | ||
| # verify PCRE2 at install time, where the failure names the actual problem. | ||
| - name: Install ripgrep (pinned + checksum-verified, PCRE2 required) | ||
| env: | ||
| RIPGREP_VERSION: "14.1.1" | ||
| RIPGREP_SHA256: "4cf9f2741e6c465ffdb7c26f38056a59e2a2544b51f7cc128ef28337eeae4d8e" | ||
| run: | | ||
| 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 >/dev/null \ | ||
| || { echo "::error title=public-repo-guard::this ripgrep build lacks PCRE2 (required for the -P policy rules) — install a PCRE2-enabled rg."; exit 1; } | ||
|
|
||
| - 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 | ||
Uh oh!
There was an error while loading. Please reload this page.