ci: scan issue and comment bodies — this repo has never scanned one - #28
ci: scan issue and comment bodies — this repo has never scanned one#28yakimoto wants to merge 12 commits into
Conversation
…ment body Measured across all 28 public wave-av repos (claude-workstation#1747, #1794): TWO coverage shapes satisfy the one required check name `Secrets + content policy`. 27 repos triggers: pull_request, push, workflow_dispatch jobs: guard 1 repo triggers: + issues, issue_comment jobs: + body-guard This repo is in the 27. All 28 report the same green check. The outlier is wave-moq-edge, and its own comment says why it matters: "`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." A PR/issue/comment BODY is exactly as world-readable as the tree, and until now it was scanned by nothing server-side. That gap was not theoretical on wave-moq-edge: 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. WHAT LANDS HERE — the bundle the workflow's own header names, minus what this repo already has (.gitleaks.toml and content-policy.sh are already vendored): .github/workflows/public-repo-guard.yml replaced (73 -> 163 lines) scripts/public-repo-guard/body-policy.sh new, mode 100755 scripts/public-repo-guard/tests/body-policy.test.sh new, mode 100755 Copied from wave-moq-edge, which has run this shape in production. Modes preserved via the git trees API — the contents API would have created both scripts 100644. HONEST ABOUT WHAT IT CAN 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 says go redact, fast. Only a client-side pre-write hook stops that class before publication. Also inherited from the reference: concurrency moves from workflow-level to PER JOB, because the two jobs want opposite behaviour. A workflow-level group forced one policy on both, and rapid body edits cancelled the tree job repeatedly — every cancelled check-run stays attached to the commit, so the PR reported UNSTABLE while the live runs were green. The body gate ships with its own fixtures and runs them in CI. Its NEGATIVE cases are the load-bearing half: a leak gate that blocks legitimate cross-repo references gets switched off, and then it protects nothing. Refs wave-av/claude-workstation#1747. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Important Review skippedAuto reviews are limited based on label configuration. 🚫 Excluded labels (none allowed) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_33fb7ca8-2c36-4f2c-a631-a17de748eb5d) |
ApprovabilityVerdict: Needs human review Unable to check for correctness in e7c8d61. This PR introduces a new security scanning feature with CI workflows that gate PR merges, complex regex-based leak detection, and changes to merge blocking behavior. Security-related new capabilities warrant human review regardless of code ownership. You can customize Macroscope's approvability policy. Learn more. |
PR Summary by Qodoci: add issue/comment body scanning to public-repo-guard
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
| raw="$(rg -nP --no-filename -- "$re" "$FILE" 2>/dev/null)"; rc=$? | ||
| if (( rc >= 2 )); then | ||
| echo "::error title=public-repo-guard ($name)::ripgrep failed (exit $rc) scanning rule '$name' — failing closed." | ||
| exit 2 | ||
| fi |
There was a problem hiding this comment.
📝 Info: PCRE2 is required and Ubuntu 22.04's ripgrep package does not have it
Every rule uses -P. Verified: the ubuntu-24.04 runner image does not preinstall ripgrep, so this step apt-installs it; the noble ripgrep package (14.1.0) links libpcre2-8-0 and works, but the jammy package (13.0.0-2ubuntu0.1) is built WITHOUT PCRE2 (confirmed locally: rg --pcre2-version reports unavailable). Consequence: any repo that copies this template and pins runs-on: ubuntu-22.04 gets a permanently red, merge-blocking Body content policy check (fail-closed at scripts/public-repo-guard/body-policy.sh:32), and the tree job's new self-test step fails too. The same is true for local runs on 22.04 — the fixture suite reports 25 failures there. The explicit probe message makes this diagnosable, but a note in the workflow header ("requires a PCRE2-enabled ripgrep; ubuntu-22.04's apt package lacks it") would save the next operator the investigation.
Was this helpful? React with 👍 or 👎 to provide feedback.
Code Review by Qodo
1.
|
Qodo Fixer🍒 Ready to be cherry-picked — ✅ Merged (0) · ☑ Fixed (1) 🔗 Fix PR: #29 This fix PR was closed automatically. Its branch is preserved so you can cherry pick the changes into the original PR. Prompt for coding agent Process — 1 fixed
|
… sync template Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…o prose rules Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
There was a problem hiding this comment.
2 issues found and verified against the latest diff
Confidence score: 2/5
- In
.github/workflows/public-repo-guard.yml, a failed body scan may not actually gate merges unlessBody content policyis explicitly required, and title/body-only edits can bypass the existing required tree check; this creates a concrete policy-enforcement gap on public PRs — addBody content policyto required status checks and ensure it runs on metadata-only edits. - In
.github/workflows/public-repo-guard.yml, installing ripgrep via apt means the runner may get a build without PCRE2, whilebody-policy.shrules depend on that feature; scans can silently misbehave or fail inconsistently across environments — pin a known-good ripgrep version (or verify PCRE2 support at runtime) before running policy checks.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name=".github/workflows/public-repo-guard.yml">
<violation number="1" location=".github/workflows/public-repo-guard.yml:114">
P1: A failing body scan will not block a PR unless `Body content policy` is added to the repository's required status checks; on body/title edits the existing required tree check is skipped and can remain green. Configure this new check as required, or make the existing required gate depend on it, to enforce the stated PR-blocking behavior.</violation>
<violation number="2" location=".github/workflows/public-repo-guard.yml:135">
P2: The workflow installs ripgrep via `apt-get install -y -qq ripgrep`, which pulls whatever version the runner image's distro package provides with no guarantee of PCRE2 support. Since every rule in body-policy.sh relies on `rg -nP` (PCRE2 regex), a non-PCRE2 build of ripgrep causes every rule to error out with 'PCRE2 is not available in this build of ripgrep', which check() treats as a scanner error (exit 2) — failing the job on every body scan with an opaque message. Consider asserting PCRE2 support explicitly (e.g. `rg -P --version`) before running the checks so failures are diagnosable.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
| # 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 |
There was a problem hiding this comment.
P1: A failing body scan will not block a PR unless Body content policy is added to the repository's required status checks; on body/title edits the existing required tree check is skipped and can remain green. Configure this new check as required, or make the existing required gate depend on it, to enforce the stated PR-blocking behavior.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/public-repo-guard.yml, line 114:
<comment>A failing body scan will not block a PR unless `Body content policy` is added to the repository's required status checks; on body/title edits the existing required tree check is skipped and can remain green. Configure this new check as required, or make the existing required gate depend on it, to enforce the stated PR-blocking behavior.</comment>
<file context>
@@ -71,3 +92,72 @@ jobs:
+ # 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'
+ concurrency:
</file context>
| sparse-checkout: scripts/public-repo-guard | ||
| sparse-checkout-cone-mode: false | ||
|
|
||
| - name: Install ripgrep |
There was a problem hiding this comment.
P2: The workflow installs ripgrep via apt-get install -y -qq ripgrep, which pulls whatever version the runner image's distro package provides with no guarantee of PCRE2 support. Since every rule in body-policy.sh relies on rg -nP (PCRE2 regex), a non-PCRE2 build of ripgrep causes every rule to error out with 'PCRE2 is not available in this build of ripgrep', which check() treats as a scanner error (exit 2) — failing the job on every body scan with an opaque message. Consider asserting PCRE2 support explicitly (e.g. rg -P --version) before running the checks so failures are diagnosable.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/public-repo-guard.yml, line 135:
<comment>The workflow installs ripgrep via `apt-get install -y -qq ripgrep`, which pulls whatever version the runner image's distro package provides with no guarantee of PCRE2 support. Since every rule in body-policy.sh relies on `rg -nP` (PCRE2 regex), a non-PCRE2 build of ripgrep causes every rule to error out with 'PCRE2 is not available in this build of ripgrep', which check() treats as a scanner error (exit 2) — failing the job on every body scan with an opaque message. Consider asserting PCRE2 support explicitly (e.g. `rg -P --version`) before running the checks so failures are diagnosable.</comment>
<file context>
@@ -71,3 +92,72 @@ jobs:
+ 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)
+
</file context>
…ing concurrency Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…very later comment Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…b superseded a failing required check Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
| issues: | ||
| types: [opened, edited] | ||
| issue_comment: | ||
| types: [created, edited] |
There was a problem hiding this comment.
🔍 Body verdicts from issue/comment/review events never land on the PR head SHA
Only pull_request events (opened/edited/reopened/synchronize) produce a check run against the PR head commit. For issue_comment and issues, GITHUB_SHA is the default-branch head, and for pull_request_review/pull_request_review_comment it 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 red public-repo-guard / Body content policy check run to the head of main. The header at .github/workflows/public-repo-guard-body.yml:24-27 frames 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.
…he alarm, not a bug Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…job policy provenance The fixture file pinned three real-looking private repo names into the one path neither gate scans (content-policy.sh excludes scripts/public-repo-guard/ and .gitleaks.toml allowlists it), against body-policy.sh's own 'names are NOT hardcoded' rule. The test names are now clearly fictitious and the file says why. The tree job's use of the PR's own content-policy.sh is now documented as deliberate: on pull_request events the workflow definition itself runs from the PR merge ref, so pinning the script to base removes no attacker capability, and the tree gate must scan and self-test the policy version the tree ships so script and fixtures can change together. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_71827916-8955-42aa-9570-2cc1d98d88bf) |
With [A-Z0-9]* the class excluded _, so for a multi-segment name like WAVE_VIEWPORT_LEASE_SECRET only the trailing LEASE_SECRET could match, and its start sits right after a _ (a word character) — the \b prepended in the name-then-detail alternative was unsatisfiable, silently passing that order. The detail-then-name order has no leading \b, which is why the existing fixtures stayed green. Adds a regression fixture for the forward order, plus a fixture pinning the deliberate about-exempt behaviour of the proximity rule (a line that names the gate is dropped even when it carries the leak shape), which was previously untested. Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…hat reddens docs paths gets switched off Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
…a line-scoped rule Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
| # The proximity gap: any character INSIDE a paragraph. A newline is allowed | ||
| # only when not followed by another (possibly whitespace-padded) blank line, | ||
| # so hard wraps and bullets stay in scope while a paragraph break resets it. | ||
| _GAP='(?:[^\n]|\n(?![ \t]*\n)){0,140}?' |
There was a problem hiding this comment.
🔴 Legitimate pull-request descriptions get blocked because Windows-style line endings defeat the paragraph boundary
The gap between a private repo name and operational detail is allowed to cross a blank line whenever the blank line uses Windows-style endings (\n(?![ \t]*\n) at scripts/public-repo-guard/body-policy.sh:202), so ordinary descriptions that mention a private repo in one paragraph and a credential name in the next are wrongly reported as leaks.
Impact: Authors of perfectly clean pull requests see a red required check they cannot fix, and the gate quickly loses credibility.
CR characters make the paragraph-break lookahead fail
GitHub delivers PR/issue/comment bodies in the event payload with CRLF line endings, and jq -r writes them out verbatim (.github/workflows/public-repo-guard-body.yml:134-140). The gap regex allows a newline only when it is not followed by another (whitespace-padded) newline, but [ \t] does not include \r. For a real paragraph break \r\n\r\n, the lookahead sees \r\n after the first \n, does not match, so the negative lookahead succeeds and the newline is consumed as gap — proximity is never reset.
Reproduced locally with rg 14.1.1: the exact fixture from scripts/public-repo-guard/tests/body-policy.test.sh:101-104 (expected exit 0) exits 1 when written with CRLF. The fixtures only use LF, so CI never catches it.
Fix options: allow \r in the lookahead padding, or strip CRs when materializing the body in the workflow (and add a CRLF fixture).
| # The proximity gap: any character INSIDE a paragraph. A newline is allowed | |
| # only when not followed by another (possibly whitespace-padded) blank line, | |
| # so hard wraps and bullets stay in scope while a paragraph break resets it. | |
| _GAP='(?:[^\n]|\n(?![ \t]*\n)){0,140}?' | |
| # The proximity gap: any character INSIDE a paragraph. A newline is allowed | |
| # only when not followed by another (possibly whitespace-padded) blank line, | |
| # so hard wraps and bullets stay in scope while a paragraph break resets it. | |
| # `\r` is part of the padding class: GitHub bodies arrive CRLF-terminated. | |
| _GAP='(?:[^\n]|\n(?![ \t\r]*\n)){0,140}?' |
Was this helpful? React with 👍 or 👎 to provide feedback.
| **Install + enforce:** copy the six files the workflow's header lists — | ||
| `public-repo-guard.yml` AND `public-repo-guard-body.yml` into | ||
| `.github/workflows/`, plus `.gitleaks.toml`, | ||
| `scripts/public-repo-guard/content-policy.sh`, | ||
| `scripts/public-repo-guard/body-policy.sh`, and | ||
| `scripts/public-repo-guard/tests/body-policy.test.sh` — then add BOTH check | ||
| names, `public-repo-guard / Secrets + content policy` and | ||
| `public-repo-guard / Body content policy`, to the branch's required status | ||
| checks. The tree check alone does not gate body edits: those only trigger the | ||
| body workflow, so without the second required check a failing body scan leaves | ||
| the PR mergeable. |
There was a problem hiding this comment.
🟡 Changelog not updated for a user-facing change
The repository's agent contract requires the Unreleased section of CHANGELOG.md to be updated with user-facing changes, but this PR adds a new required merge gate and installation instructions without any changelog entry (workflow-templates/README.md:49-68).
Impact: Consumers of the org templates get a new mandatory check with no changelog record of it.
Rule source
AGENTS.md — Conventions: "Update CHANGELOG.md (Unreleased) with user-facing changes." The PR adds public-repo-guard-body.yml, body-policy.sh and changes the install/enforce instructions, which are user-facing for every repo installing the gate.
Was this helpful? React with 👍 or 👎 to provide feedback.
…AGENTS.md requires Co-authored-by: Codesmith <codesmith-bot@users.noreply.github.com>
This repo's
public-repo-guardhas never scanned a single issue or comment body.Measured across all 28 public wave-av repos (
wave-av/claude-workstation#1747,#1794): two coverage shapes satisfy the one required check nameSecrets + content policy.pull_request, push, workflow_dispatchguardissues,issue_commentbody-guardThis repo is in the 27. All 28 report the same green check — because a required check asserts that something named X passed, never what X examined.
The outlier is
wave-moq-edge, and its own comment says why it matters:That gap was not theoretical there: a PR was blocked for naming a private repo in
wrangler.tomlwhile the very same name, with more operational detail attached, sat unchallenged in its body.What lands
Three files — the bundle the workflow's own header names, minus what this repo already has (
.gitleaks.tomlandcontent-policy.share already vendored, and are checked as prerequisites; a repo missing either is refused rather than half-installed):The workflow's header names four files as the install unit but executes a fifth —
tests/body-policy.test.sh, in its own self-test step. Omitting it installs a workflow that fails on a step nobody read, so the manifest ships it. Modes are preserved via the git trees API; the contents API creates100644regardless, which would silently break running these scripts as executables.Planned by
governance/lib/vendor-bundle.mjs(claude-workstation#1850) against a checked-in manifest, not by ad-hoc shell.One deliberate divergence from the reference, stated rather than silent
The shipped workflow is
wave-moq-edge's withactions/checkoutbumped from v5.0.1 to v7.0.1 (3d3c42e5aac5ba805825da76410c181273ba90b1), the pin already used byclaude-workstation's own gate.Copying verbatim was checked first and rejected on evidence: of the 18 target repos, 17 carry a byte-identical guard, and
wave-realtime-edgealready runs v7.0.0 — so a verbatim copy would have downgraded it, and shipped a stale pin to the other 17. A separate PR brings the reference itself up to the same pin.Honest about what this 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 says go redact, fast. Only a client-side pre-write hook stops that class before publication.
Also inherited from the reference: concurrency moves from workflow-level to per job, because the two jobs want opposite behaviour. A workflow-level group forced one policy on both, and rapid body edits cancelled the tree job repeatedly — every cancelled check-run stays attached to the commit, so the PR reported UNSTABLE while the live runs were green.
The body gate ships with its own fixtures and runs them in CI. Its negative cases are the load-bearing half: a leak gate that blocks legitimate cross-repo references gets switched off, and then it protects nothing.
Refs
wave-av/claude-workstation#1747.Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is enabled.Note
Medium Risk
Changes required CI behavior and branch-protection semantics (skipped checks superseding failures); misconfiguration could leave PRs mergeable on failing body scans until the second check is required. The body gate is security-sensitive but uses fail-closed scanning and trusted-base policy execution.
Overview
Adds server-side scanning for PR/issue/comment/review text via a new
Body content policyjob, alongside hardening the existing tree gate so required checks cannot be cleared by skipped or cancelled runs.New body half:
public-repo-guard-body.ymlrunsbody-policy.shon materialized event text (not shell-interpolated), with triggers for PRedited/synchronize, reviews, review comments, issues, and issue comments. Policy runs from a trusted base sparse checkout so a PR cannot ship a no-op scanner; install PRs fall back to the incoming copy with a warning.body-policy.shmirrors tree leak classes (credentials, infra IDs, internal markers) but uses proximity rules for private repo names so bare cross-references stay allowed. Annotations redact matched content.Tree workflow changes: Splits from the body job into
public-repo-guard.ymlonly — drops PRedited, moves concurrency to a tree-only cancelling group, bumpsactions/checkoutto v7.0.1, and runsbody-policy.test.shin CI. Docs and workflow templates now describe a six-file install and require both required check names.Operational note: Merging this workflow alone does not add branch protection for body edits; repos must also require
public-repo-guard / Body content policy.Reviewed by Cursor Bugbot for commit 351b528. Configure here.
Note
Add CI body scanning to detect credential and policy violations in PR/issue/comment text
jq, and runs the body policy script from a trusted checkout.editedfrom tree-scan triggers (body edits now handled separately) and runs body-policy fixture tests as part of the tree job.public-repo-guard / Tree content policyandpublic-repo-guard / Body content policycheck names to enforce the full guard.Macroscope summarized e7c8d61.
Review follow-up (dba6c63)
Three hardenings from review:
pull_request_reviewandpull_request_review_commentnow trigger the body gate, and.review.bodyis projected: review summaries and inline diff comments were separate, unscanned surfaces.body-guardexecutes the policy from the trusted BASE revision, not the PR merge ref, so a PR cannot rewrite the gate that judges it. The one exception is the install PR itself (base has no policy yet), which runs its own copy with a visible warning; that path is unreachable in an installed repo.cancel-in-progress: false.The README install instructions now require BOTH check names (
Secrets + content policyandBody content policy); the tree check alone is skipped oneditedevents, so without the second a failing body scan leaves the PR mergeable.Review follow-up (0e6354e)
The tree scan and the body gate are now two workflow FILES (
public-repo-guard.ymlandpublic-repo-guard-body.yml, sharing thepublic-repo-guarddisplay name so both required check contexts read unchanged). Review found that the single-file shape was itself a bypass: a job disabled byif:still publishes a check run with a non-failingskippedconclusion, and branch protection reads the latest check run per context, so a description edit (or any review comment) let the skipped tree job supersede a FAILINGSecrets + content policyresult on the same head SHA. With separate files, a body-only event never emits a check run for the tree context. The install bundle is now six files; the README and both templates are updated.Also from review: the
internal-markerprose rule is now case-insensitive ((?i), scoped to that rule alone), since real banners are written as "INTERNAL ONLY" or "Do Not Share"; credential-format rules stay case-exact. Regression fixtures cover both the capitalised banner and a quoted capitalised mention; all 25 pass.