-
Notifications
You must be signed in to change notification settings - Fork 0
ci: scan issue and comment bodies — this repo has never scanned one #28
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
Open
yakimoto
wants to merge
12
commits into
main
Choose a base branch
from
ci/1747-public-repo-guard-body-scan
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
34ecb6a
ci: this repo's public-repo-guard never scanned a single issue or com…
yakimoto fe06bd0
ci: scope (?i) to repo names, probe PCRE2, complete install manifest,…
yakimoto 6413636
ci: fail closed on filter errors, scope about-the-control allowlist t…
yakimoto dba6c63
ci: cover review bodies, pin policy to base ref, drop pending-cancell…
yakimoto 07d1dbd
ci: scan only what the event changed, so one bad body cannot redden e…
yakimoto 0e6354e
ci: split the body gate into its own workflow file; a skipped tree jo…
yakimoto 5279e32
ci: document where detection verdicts land — a red check on main is t…
yakimoto 351b528
ci: use fictitious repo names in body-policy fixtures; document tree-…
yakimoto 5e07da9
fix: allow underscores in the proximity rule's credential-name class
yakimoto a3eb6db
ci: anchor the home-path rule and exempt do-not prose — a body gate t…
yakimoto 7f4f16b
ci: paragraph-scope the proximity gap — a hard-wrapped body defeated …
yakimoto e7c8d61
fix: let the paragraph break survive CRLF endings; add the changelog …
yakimoto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| # Same display name as public-repo-guard.yml, deliberately: the two files are one | ||
| # gate, and sharing the name keeps the check contexts reading exactly as branch | ||
| # protection requires them — `public-repo-guard / Secrets + content policy` and | ||
| # `public-repo-guard / Body content policy`. | ||
| name: public-repo-guard | ||
|
|
||
| # Pre-publication content gate for WAVE public repos — the BODY half. The tree | ||
| # workflow (public-repo-guard.yml) scans the published TREE; a PR/review/issue/ | ||
| # comment 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 FILE from the tree scan, and the split is load-bearing, not | ||
| # cosmetic. A job disabled by `if:` still publishes a check run under its context | ||
| # name with a non-failing `skipped` conclusion, and branch protection reads the | ||
| # LATEST check run per context. When both jobs shared one workflow, every | ||
| # body-only event (a description edit, a review comment) re-triggered the whole | ||
| # file, and the skipped tree job emitted a fresh `skipped` result for the same | ||
| # head SHA — superseding a FAILING `Secrets + content policy` run and making a PR | ||
| # the secret scan had rejected mergeable. Two files mean a body event can never | ||
| # produce a check run for the tree context, and vice versa. | ||
| # | ||
| # Honest about what it can and cannot do. On a PR this PREVENTS the merge. On an | ||
| # 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. | ||
| # | ||
| # WHERE a detection verdict lands is a side effect worth stating so nobody | ||
| # rediscovers it as a bug: only pull_request events attach the check run to the | ||
| # PR head SHA (the only place branch protection reads it). On `issues` and | ||
| # `issue_comment` events GITHUB_SHA is the DEFAULT-BRANCH head, so a failing | ||
| # scan pins a red `Body content policy` check run to the tip of main; review | ||
| # events pin it to the PR merge commit. Accepted, not accidental: detection has | ||
| # no merge to block, and with only a read token a red check on main is the | ||
| # loudest alarm this job can raise. Anything that reads main's combined status | ||
| # (release gating, merge queues) will see that red until a later body event | ||
| # passes under the same context — treat that as the alarm working, and redact. | ||
| # | ||
| # To install on a new repo, copy all six files together (list in the tree | ||
| # workflow's header). | ||
|
|
||
| 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` is here because this check is REQUIRED: every new head SHA needs | ||
| # a body verdict of its own or the merge box waits forever on an expected check. | ||
| pull_request: | ||
| types: [opened, edited, reopened, synchronize] | ||
| # Review summaries and inline review comments are SEPARATE events (GitHub does | ||
| # not deliver either as `issue_comment`) and are just as world-readable, so | ||
| # without these two a reviewer could publish the same content unscanned. | ||
| pull_request_review: | ||
| types: [submitted, edited] | ||
| pull_request_review_comment: | ||
| types: [created, edited] | ||
| issues: | ||
| types: [opened, edited] | ||
| issue_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 | ||
|
|
||
| # No concurrency group, deliberately: a group holds at most one PENDING run, so | ||
| # under a burst of edits the queued scans in between get cancelled even with | ||
| # cancel-in-progress false, and those body versions never receive a verdict. The | ||
| # body job is seconds long; letting every event run to completion is the cheap, | ||
| # correct option. History shows why it must not share the tree job's cancelling | ||
| # group either: when the group was workflow-level, 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: | ||
| body-guard: | ||
| name: Body content policy | ||
| # No `if:` — every trigger of this file is a body event. That is not just | ||
| # tidiness: a job-level skip would emit a `skipped` check run under this | ||
| # context, the exact superseding hazard this file exists to remove. | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| # TRUSTED policy, not the PR's. On pull_request-family events the default | ||
| # checkout is the PR merge ref, so the gate would execute whatever | ||
| # body-policy.sh the PR itself ships: a contributor could edit it to exit 0 | ||
| # (or to read what CI hands it) and this check would still report green. | ||
| # Pinning the checkout to the BASE revision means the PR under scan can | ||
| # never define the policy that judges it. | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| with: | ||
| ref: ${{ github.event.pull_request.base.sha || github.sha }} | ||
| path: trusted | ||
| # 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 PR's own copy, consulted in exactly ONE case: the base revision has no | ||
| # body policy yet, i.e. the PR that INSTALLS the gate. In an installed repo | ||
| # the base always has the script, so a hostile PR cannot reach this fallback | ||
| # by deleting or editing its copy; the trusted checkout above still wins. | ||
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | ||
| if: startsWith(github.event_name, 'pull_request') | ||
| with: | ||
| path: incoming | ||
| sparse-checkout: scripts/public-repo-guard | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Install ripgrep | ||
| run: command -v rg >/dev/null || (sudo apt-get update -qq && sudo apt-get install -y -qq ripgrep) | ||
|
|
||
| # 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 | ||
| # Scan WHAT THIS EVENT CHANGED, not the whole thread. Each surface gets | ||
| # its own verdict on its own event (a PR/issue body on opened/edited, a | ||
| # review on submitted/edited, a comment on created/edited). Folding the | ||
| # parent body into every comment scan would let one bad, unedited body | ||
| # keep every later comment red regardless of its content — exactly the | ||
| # permanent-noise failure mode that gets a gate switched off. | ||
| jq -r 'if has("comment") then [.comment.body] | ||
| elif has("review") then [.review.body] | ||
| elif has("pull_request") then [.pull_request.title, .pull_request.body] | ||
| else [.issue.title, .issue.body] | ||
| end | ||
| | 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 }} | ||
| # The existence assertion turns "the sparse checkout resolved to nothing" | ||
| # into a named failure instead of bash's opaque "No such file or directory". | ||
| run: | | ||
| POLICY="trusted/scripts/public-repo-guard/body-policy.sh" | ||
| if [ ! -f "$POLICY" ]; then | ||
| if [ -f "incoming/scripts/public-repo-guard/body-policy.sh" ]; then | ||
| echo "::warning title=public-repo-guard (body-guard)::base revision has no body policy; running the copy this PR installs. Tamper protection begins once it merges." | ||
| POLICY="incoming/scripts/public-repo-guard/body-policy.sh" | ||
| else | ||
| echo "::error title=public-repo-guard (body-guard)::body-policy.sh missing from the trusted checkout; failing closed." | ||
| exit 2 | ||
| fi | ||
| fi | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
| bash "$POLICY" "$RUNNER_TEMP/bodyscan/body.txt" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| # Changelog | ||
|
|
||
| User-facing changes to this repository's org-wide workflow templates and vendored | ||
| guard scripts. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). | ||
|
|
||
| ## Unreleased | ||
|
|
||
| ### Added | ||
|
|
||
| - `public-repo-guard` now has a BODY gate: a second workflow file | ||
| (`public-repo-guard-body.yml`) plus `scripts/public-repo-guard/body-policy.sh` | ||
| scan pull-request titles/bodies, issue titles/bodies, comments, and review | ||
| bodies for the same leak classes the tree gate covers (credential formats, | ||
| infrastructure identifiers, internal markers, and private-repo names near | ||
| operational detail). Previously nothing scanned these world-readable surfaces | ||
| server-side. | ||
|
|
||
| ### Changed | ||
|
|
||
| - Installing `public-repo-guard` in a repo now means vendoring six files and | ||
| requiring BOTH check names (`public-repo-guard / Secrets + content policy` | ||
| and `public-repo-guard / Body content policy`) in branch protection; the tree | ||
| check alone does not gate body edits. See `workflow-templates/README.md` for | ||
| the install and enforce steps. |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 Body verdicts from issue/comment/review events never land on the PR head SHA
Only
pull_requestevents (opened/edited/reopened/synchronize) produce a check run against the PR head commit. Forissue_commentandissues, GITHUB_SHA is the default-branch head, and forpull_request_review/pull_request_review_commentit is the PR merge commit — so a FAILING body scan from those events is invisible to branch protection and, for issue/issue_comment, attaches a redpublic-repo-guard / Body content policycheck run to the head ofmain. The header at.github/workflows/public-repo-guard-body.yml:24-27frames comment/issue coverage as detection-only, which is consistent, but the side effect of reddening the default-branch commit status (and any tooling that reads it, e.g. release gating or merge queues) is worth confirming is acceptable.Was this helpful? React with 👍 or 👎 to provide feedback.