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
98 changes: 94 additions & 4 deletions .github/workflows/public-repo-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ 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 four files together:
# .github/workflows/public-repo-guard.yml
# .gitleaks.toml
# scripts/public-repo-guard/content-policy.sh
# scripts/public-repo-guard/body-policy.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,21 +26,41 @@ 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:
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'
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
Expand Down Expand Up @@ -71,3 +92,72 @@ jobs:
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 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 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.
body-guard:
name: Body content policy
if: github.event_name == 'pull_request' || github.event_name == 'issues' || github.event_name == 'issue_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.
#
# 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.pull_request.number || github.event.comment.id || github.event.issue.number || github.ref }}
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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
Comment on lines +128 to +133

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Security Misconfiguration (CWE-522): Insufficiently Protected Credentials

Reachability: Internal

Set persist-credentials: false on the body-guard checkout.

This checkout doesn't need to push anything, but by default actions/checkout persists the job's token in .git/config, making it readable by any later step (or a compromised dependency) in this job. Since permissions: contents: read already limits scope, this is defense-in-depth, but it's a one-line, zero-downside hardening on a job that scans untrusted text.

🔒 Suggested fix
       - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd  # v5.0.1
         with:
+          persist-credentials: false
           sparse-checkout: scripts/public-repo-guard
           sparse-checkout-cone-mode: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.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
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
with:
persist-credentials: false
# 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
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 128-133: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/public-repo-guard.yml around lines 128 - 133, Update the
body-guard checkout step using actions/checkout to set persist-credentials to
false in its with configuration, while preserving the existing sparse-checkout
settings.

Source: Linters/SAST tools


- 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")' "$GITHUB_EVENT_PATH")" != "true" ]; then
echo "::error title=public-repo-guard (body-guard)::Event payload contains no pull_request/issue/comment 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]
| 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: bash scripts/public-repo-guard/body-policy.sh "$RUNNER_TEMP/bodyscan/body.txt"
139 changes: 139 additions & 0 deletions scripts/public-repo-guard/body-policy.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
#!/usr/bin/env bash
# WAVE public-repo BODY policy — the internal-leak gate for PR/issue/comment text.
#
# Companion to content-policy.sh. That script scans the published working TREE;
# this one scans the other half of a public repo's surface: pull-request titles
# and bodies, issue bodies, and comment bodies. Those are equally world-readable
# and, until this script existed, were scanned by NOTHING server-side. That gap
# was not theoretical — a PR was merged whose wrangler.toml was correctly BLOCKED
# for naming a private repo while the PR body named the same repo, with more
# operational detail attached, and sailed through.
#
# Usage: scripts/public-repo-guard/body-policy.sh <file>
# <file> holds the untrusted text, already materialized to disk. It is passed as
# a PATH and only ever read — the body is never interpolated into a command line
# or an environment variable, so no amount of shell metacharacters in a PR body
# can influence what runs here.
#
# Exit: 0 clean · 1 blocking violation · 2 scanner error (fail closed).
#
# Allowlisting: a line carrying `guard:allow <reason>` is exempt (an accidental
# leak never carries the marker; a deliberate one is visible in a public diff), as
# is any line matching the ABOUT-THE-CONTROL allowlist below.
set -uo pipefail

FILE="${1:-}"
[[ -n "$FILE" && -f "$FILE" ]] || { echo "::error::body-policy: usage: body-policy.sh <file>"; exit 2; }
command -v rg >/dev/null 2>&1 || { echo "::error::body-policy: ripgrep (rg) required"; exit 2; }

VIOLATIONS=0

# Lines that TALK ABOUT the control rather than leaking through it. Without this,
# the gate blocks its own pull requests and every security discussion — the
# self-referential trap that gets a gate switched off. Ported verbatim in intent
# from the client-side gate's allowlist, which was built for exactly this.
ABOUT_THE_CONTROL='(public-repo-guard|body-policy|content-policy|public-github-write-gate|\bNDA\s+(gate|guard|policy|denylist|sweep|scan|hook)\b|\bno\s+NDA\b|responsib\w*\s+disclos|SECURITY\.md)'

# check <BLOCK|WARN> <name> <regex> <why>
check() {
local sev="$1" name="$2" re="$3" why="$4"
[[ -z "$re" ]] && { echo "::error::body-policy: internal bug — empty regex for rule '$name'"; exit 2; }
# rg exit: 0=match, 1=no match, >=2=real error → FAIL CLOSED. A gate that passes
# because its scanner broke is worse than no gate: it reports success.
local raw rc
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
# Filter with rg, not grep: BSD/macOS grep has no -P, so a `grep -P` allowlist
# silently errors out locally while working on GNU/CI — the gate would then
# disagree with itself depending on where it ran. rg is already required above.
local matches
matches="$(printf '%s' "$raw" \
| rg -vN -- 'guard:allow[[:space:]]+[^[:space:]]' \
| rg -vNiP -- "$ABOUT_THE_CONTROL" || true)"
[[ -z "$matches" ]] && return 0
local count; count="$(printf '%s\n' "$matches" | grep -c '')"
# Print the LINE NUMBER only — never the matched text. This annotation is itself
# world-readable, so echoing the hit would re-publish the very thing we caught.
echo "::group::[$sev] $name — $why"
printf '%s\n' "$matches" | sed -E 's/^([0-9]+):.*/ line \1: «match redacted — view the body to see it»/'
echo "::endgroup::"
if [[ "$sev" == "BLOCK" ]]; then
echo "::error title=public-repo-guard ($name)::$why — $count occurrence(s) in the title/body. Edit the body to remove it, then re-run."
VIOLATIONS=$((VIOLATIONS+1))
else
echo "::warning title=public-repo-guard ($name)::$why — $count occurrence(s) (non-blocking; review)."
fi
}

# --- Credential formats — never legitimate in prose --------------------------
check BLOCK stripe-live-key '(sk|rk)_live_[A-Za-z0-9]{16,}' 'Live Stripe secret/restricted key'
check BLOCK stripe-account 'acct_[A-Za-z0-9]{16,}' 'Live Stripe account ID — financial infra, never publish'
check BLOCK anthropic-key 'sk-ant-(api|admin)[0-9]{2}-[A-Za-z0-9_-]{20,}' 'Real Anthropic API/admin key'
check BLOCK github-pat 'github_pat_[A-Za-z0-9_]{30,}' 'GitHub fine-grained PAT'
check BLOCK supabase-pat 'sbp_[a-f0-9]{40}' 'Supabase personal access token'
check BLOCK aws-akid 'AKIA[0-9A-Z]{16}' 'AWS access key ID'
check BLOCK private-key '-----BEGIN [A-Z ]*PRIVATE KEY-----' 'Embedded private key material'
Comment on lines +38 to +78

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Other (CWE-807)

Reachability: External

guard:allow bypasses hard credential-format BLOCKs too — not just the judgment-call rules.

The guard:allow <reason> filter at Line 54 runs unconditionally inside check(), before any rule-specific logic, so it exempts a whole line from every rule that scans it — including the format-specific credential checks at Lines 72-78 (stripe-live-key, stripe-account, anthropic-key, github-pat, supabase-pat, aws-akid, private-key). A line like sk_live_XXXXXXXXXXXXXXXX guard:allow debug fully defeats the live-Stripe-key block.

The header (Lines 20-22) and the internal-marker section (Lines 97-99) explicitly justify this tradeoff for context-dependent judgment calls ("a deliberate one is visible in a public diff"), but that rationale doesn't hold for hard credential formats: there is no legitimate reason a real live key/PAT/private-key match should ever be allowed through with a comment, and unlike the internal-marker case, exposure already happens the instant the text is posted regardless of whether the check blocks the PR. This scenario also isn't covered by any fixture in body-policy.test.sh.

🔒 Suggested fix: opt specific rules out of the guard:allow filter
-check() {
-  local sev="$1" name="$2" re="$3" why="$4"
+check() {
+  local sev="$1" name="$2" re="$3" why="$4" allow_override="${5:-yes}"
   ...
   local matches
-  matches="$(printf '%s' "$raw" \
-    | rg -vN -- 'guard:allow[[:space:]]+[^[:space:]]' \
-    | rg -vNiP -- "$ABOUT_THE_CONTROL" || true)"
+  matches="$raw"
+  if [[ "$allow_override" == "yes" ]]; then
+    matches="$(printf '%s' "$matches" | rg -vN -- 'guard:allow[[:space:]]+[^[:space:]]' || true)"
+  fi
+  matches="$(printf '%s' "$matches" | rg -vNiP -- "$ABOUT_THE_CONTROL" || true)"

Then call the hard-format rules with the override disabled, e.g. check BLOCK stripe-live-key '...' '...' no.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/public-repo-guard/body-policy.sh` around lines 38 - 78, Update
check() to accept an optional parameter controlling whether the guard:allow
filter applies, defaulting to the current bypass behavior for judgment-call
rules. Pass the override-disabled value when invoking the hard credential-format
rules stripe-live-key, stripe-account, anthropic-key, github-pat, supabase-pat,
aws-akid, and private-key, so guard:allow cannot suppress those matches; add or
update body-policy.test.sh fixtures to verify this behavior.


# --- Infrastructure identifiers ----------------------------------------------
# shellcheck disable=SC2016 # $CLOUDFLARE_ACCOUNT_ID is literal guidance text
check BLOCK cf-account-id 'account_id\s*[:=]\s*["'"'"']?[0-9a-f]{32}' 'Hardcoded Cloudflare account_id — reference the env var instead'
check BLOCK internal-ip '100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.[0-9]{1,3}\.[0-9]{1,3}' 'Internal Tailscale-CGNAT IP (100.64.0.0/10) — internal fleet address'
# shellcheck disable=SC2016 # $HOME is literal guidance text
check BLOCK abs-user-path '/(Users|home)/(?!runner/)[a-z][a-z0-9._-]+/' 'Operator absolute home path — leaks identity and local layout'
Comment on lines +81 to +85

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale shellcheck disable=SC2016 comments don't match the code they annotate.

Both disables claim a literal $CLOUDFLARE_ACCOUNT_ID/$HOME reference justifies suppressing SC2016, but neither the regex nor the "why" text on Lines 82/85 actually contains those variable names — nothing here would trigger SC2016 in the first place. This looks like the "why" messages once told the operator which env var to use and that guidance was dropped, leaving a dangling disable comment and a less actionable annotation ("reference the env var instead" without naming it).

✏️ Suggested fix
-# shellcheck disable=SC2016  # $CLOUDFLARE_ACCOUNT_ID is literal guidance text
 check BLOCK cf-account-id    'account_id\s*[:=]\s*["'"'"']?[0-9a-f]{32}'      'Hardcoded Cloudflare account_id — reference the env var instead'
-# shellcheck disable=SC2016  # $HOME is literal guidance text
 check BLOCK abs-user-path    '/(Users|home)/(?!runner/)[a-z][a-z0-9._-]+/'    'Operator absolute home path — leaks identity and local layout'

Or, if the intent was to actually name the env var in the message, restore that instead of removing the disable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# shellcheck disable=SC2016 # $CLOUDFLARE_ACCOUNT_ID is literal guidance text
check BLOCK cf-account-id 'account_id\s*[:=]\s*["'"'"']?[0-9a-f]{32}' 'Hardcoded Cloudflare account_id — reference the env var instead'
check BLOCK internal-ip '100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.[0-9]{1,3}\.[0-9]{1,3}' 'Internal Tailscale-CGNAT IP (100.64.0.0/10) — internal fleet address'
# shellcheck disable=SC2016 # $HOME is literal guidance text
check BLOCK abs-user-path '/(Users|home)/(?!runner/)[a-z][a-z0-9._-]+/' 'Operator absolute home path — leaks identity and local layout'
check BLOCK cf-account-id 'account_id\s*[:=]\s*["'"'"']?[0-9a-f]{32}' 'Hardcoded Cloudflare account_id — reference the env var instead'
check BLOCK internal-ip '100\.(6[4-9]|[7-9][0-9]|1[01][0-9]|12[0-7])\.[0-9]{1,3}\.[0-9]{1,3}' 'Internal Tailscale-CGNAT IP (100.64.0.0/10) — internal fleet address'
check BLOCK abs-user-path '/(Users|home)/(?!runner/)[a-z][a-z0-9._-]+/' 'Operator absolute home path — leaks identity and local layout'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/public-repo-guard/body-policy.sh` around lines 81 - 85, Remove the
stale shellcheck disable comments immediately above the cf-account-id and
abs-user-path checks, since their regexes and messages no longer reference shell
variables or trigger SC2016. Keep both BLOCK rules and their existing detection
patterns and messages unchanged.


# --- Self-identified internal material ---------------------------------------
# USE vs MENTION. A body that SAYS "internal-only" is leaking; a body that QUOTES
# the phrase is describing a policy — including this one. The lookarounds exempt a
# marker wrapped in straight, smart, or backtick quotes.
#
# Not hypothetical: the first run of this job failed on its own pull request,
# because a review bot had edited the PR body to summarize the change and its
# summary quoted the phrase verbatim. The line-level allowlist could not help —
# that line named no gate. Only use-vs-mention separates the two.
#
# A quoted marker is also a trivial bypass, and that is an accepted trade. The
# threat here is the ACCIDENTAL paste; a deliberate evader has easier routes, and
# `guard:allow <reason>` already exists as the honest, visible one.
check BLOCK internal-marker '(?<![“"'"'"'`])\b(internal[- ]only|do\s+not\s+(share|publish|distribute)|for\s+internal\s+use)\b(?![”"'"'"'`])' 'Text self-identifies as not-for-public'
Comment on lines +87 to +100

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Extract only the internal-marker check line and replay it through a stub
# check() to see the literal regex bash produces (no repo file is executed).
LN=$(grep -n "check BLOCK internal-marker" scripts/public-repo-guard/body-policy.sh | cut -d: -f1)
LINE=$(sed -n "${LN}p" scripts/public-repo-guard/body-policy.sh)
echo "extracted line:"; echo "$LINE"
check() { printf 'REGEX=%s\n' "$3" > /tmp/marker_regex.txt; }
eval "$LINE"
echo "---"; cat /tmp/marker_regex.txt

if command -v rg >/dev/null && rg --pcre2-version >/dev/null 2>&1; then
  RE=$(sed -n 's/^REGEX=//p' /tmp/marker_regex.txt)
  for s in "This is 'internal-only' text." "This is ‘internal-only’ text." "This is internal-only text."; do
    printf '%s\n' "$s" > /tmp/t.txt
    echo -n "[$s] -> "
    rg -nP -- "$RE" /tmp/t.txt || echo "no match (exempt)"
  done
fi

Repository: wave-av/wave-profiles

Length of output: 654


Add / to the quote-exemption class. The pattern already exempts straight single quotes, but ‘internal-only’ still matches. If smart single quotes should count as quoted markers too, include them in the lookaround class.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/public-repo-guard/body-policy.sh` around lines 87 - 100, Update the
internal-marker regular expression in the BLOCK check to include left and right
smart single quotes in both lookaround quote-exemption classes, while preserving
the existing straight, smart double, and backtick quote handling.


# --- Private repo + operational detail (PROXIMITY, not bare name) ------------
# The BODY profile deliberately DIVERGES from the FILE profile here, and the
# divergence is the whole design. content-policy.sh blocks a bare private-repo
# name outright, which is right for a checked-in file. Applying that to bodies
# would be unusable: a sweep of public issues found 134 LEGITIMATE cross-repo
# references ("companion to <private-repo>#260"). A gate that fires on all of
# those gets switched off, and then it protects nothing.
#
# So a bare mention stays silent. What fires is a private repo name within ~140
# characters of INTERNAL OPERATIONAL DETAIL — a SCREAMING_CASE credential NAME, a
# secret-binding verb, a service binding, or a secret COUNT. That is the topology
# of what is wired to what, and it is the shape that actually leaked.
#
# Names are NOT hardcoded (this file is public); CI injects them via the
# GUARD_PRIVATE_REPOS variable. Unset locally → this check is skipped.
if [[ -n "${GUARD_PRIVATE_REPOS:-}" ]]; then
OPS_DETAIL='(?:[A-Z][A-Z0-9]*_(?:SECRET|TOKEN|KEY|PASSWORD)|wrangler\s+secret|secret\s+(?:is\s+)?(?:bound|binding|list)|(?:is\s+)?bound\s+on|service\s+binding|\d{2,}\s+secrets)'
_ALT=''
IFS=', ' read -r -a _PRIV <<< "$GUARD_PRIVATE_REPOS"
for _name in "${_PRIV[@]}"; do
[[ -z "$_name" ]] && continue
# Regex-escape so metacharacters in a name match literally.
_esc="$(printf '%s' "$_name" | sed -E 's/[][(){}.^$*+?|\\]/\\&/g')"
_ALT="${_ALT:+$_ALT|}${_esc}"
done
if [[ -n "$_ALT" ]]; then
# Both orders: name-then-detail and detail-then-name.
check BLOCK private-repo-ops \
"(?i)\\b(?:${_ALT})\\b[^\\n]{0,140}?\\b${OPS_DETAIL}|${OPS_DETAIL}[^\\n]{0,140}?\\b(?:${_ALT})\\b" \
'A private WAVE repo named alongside internal operational detail (credential name, secret binding, or secret count) — the wiring topology is not public'
fi
fi

if (( VIOLATIONS > 0 )); then
echo "::error::public-repo-guard: $VIOLATIONS blocking body-policy violation(s) — see annotations above."
exit 1
fi
echo "public-repo-guard: body policy OK"
Loading
Loading