Skip to content
Merged
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
119 changes: 107 additions & 12 deletions scripts/compliance-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ check_dependabot_config() {
add_finding "$repo" "dependabot" "missing-config" "error" \
"Missing \`.github/dependabot.yml\` configuration file" \
"standards/dependabot-policy.md"
return
return 0
fi

local decoded
Expand Down Expand Up @@ -688,7 +688,7 @@ check_codeowners() {
add_finding "$repo" "settings" "missing-codeowners" "error" \
"No \`CODEOWNERS\` file found — required for code owner review enforcement (pr-quality ruleset)" \
"standards/codeowners-standard.md"
return
return 0
fi

# Extract non-comment, non-blank owner lines for accurate matching.
Expand All @@ -704,7 +704,7 @@ check_codeowners() {
add_finding "$repo" "settings" "codeowners-empty" "error" \
"CODEOWNERS file has no owner lines (only comments/blank)" \
"standards/codeowners-standard.md"
return
return 0
fi

# Rule 1: @petry-projects/org-leads MUST be the first owner on every line
Expand Down Expand Up @@ -803,7 +803,7 @@ classify_sonar_s7637_exemption() {

if [ -z "$keys" ]; then
echo "missing"
return
return 0
fi

local key resourcekey base broad=0
Expand All @@ -829,31 +829,126 @@ classify_sonar_s7637_exemption() {
fi
}

# ---------------------------------------------------------------------------
# Inline NOSONAR S7637 marker on first-party caller stubs (#549)
#
# Canonical mechanism (supersedes the per-file sonar-project.properties approach):
# the inline `# NOSONAR(githubactions:S7637)` marker on the channel-pinned
# first-party reusable-ref `uses:` line travels WITH the caller-stub file, so
# adopters inherit the exemption on copy with zero per-repo properties entries.
#
# classify_inline_s7637_marker echoes exactly one verdict for a single workflow
# file's decoded content:
# present — every channel-pinned first-party reusable-ref `uses:` line carries
# the inline `NOSONAR(githubactions:S7637)` marker.
# missing — at least one channel-pinned first-party reusable-ref line lacks the
# marker; SonarCloud's githubactions:S7637 would fire on it.
# n/a — the file has no channel-pinned first-party reusable ref (it is
# SHA-pinned — which already satisfies S7637 — or carries no
# first-party reusable ref at all), so no inline marker is required.
# ---------------------------------------------------------------------------
classify_inline_s7637_marker() {
local content="$1"

# First-party reusable-workflow `uses:` lines (channel- or SHA-pinned).
local uses_lines
uses_lines=$(printf '%s\n' "$content" \
| grep -E '^[[:space:]]*uses:[[:space:]]*petry-projects/\.github(-private)?/\.github/workflows/[^@[:space:]]+@' || true)

if [ -z "$uses_lines" ]; then
echo "n/a"
return 0
fi

local line ref channel_seen=0 missing=0
while IFS= read -r line; do
[ -z "$line" ] && continue
# The pinned ref is the token after the first '@', up to whitespace/comment.
ref=$(printf '%s\n' "$line" | sed -E 's/^[^@]*@([^[:space:]#]+).*/\1/')
# A 40-char hex SHA pin already satisfies S7637 — no inline marker needed.
if printf '%s' "$ref" | grep -qE '^[0-9a-f]{40}$'; then
continue
fi
channel_seen=1
if ! printf '%s' "$line" | grep -qF 'NOSONAR(githubactions:S7637)'; then
missing=1
fi
done <<< "$uses_lines"

if [ "$channel_seen" -eq 0 ]; then
echo "n/a"
elif [ "$missing" -eq 1 ]; then
echo "missing"
else
echo "present"
fi
}

check_sonar_s7637_exemption() {
local repo="$1"

# Only relevant when the repo actually runs SonarCloud analysis.
gh_api "repos/$ORG/$repo/contents/.github/workflows/sonarcloud.yml" --jq '.name' > /dev/null 2>&1 || return
gh_api "repos/$ORG/$repo/contents/.github/workflows/sonarcloud.yml" --jq '.name' > /dev/null 2>&1 || return 0

# Canonical (#549): scan the repo's caller-stub workflows for the inline
# `# NOSONAR(githubactions:S7637)` marker. If every channel-pinned first-party
# reusable-ref stub carries it, the repo is exempt with no per-file
# sonar-project.properties entries needed.
local workflows wf wf_content wf_decoded inline_verdict
local inline_seen=0 inline_missing=0
workflows=$(gh_api "repos/$ORG/$repo/contents/.github/workflows" --jq '.[].name' 2>/dev/null || echo "")
for wf in $workflows; do
case "$wf" in *.yml | *.yaml) ;; *) continue ;; esac
wf_content=$(gh_api "repos/$ORG/$repo/contents/.github/workflows/$wf" --jq '.content' 2>/dev/null || echo "")
[ -z "$wf_content" ] && continue
wf_decoded=$(echo "$wf_content" | base64 -d 2>/dev/null || echo "")
[ -z "$wf_decoded" ] && continue
inline_verdict=$(classify_inline_s7637_marker "$wf_decoded")
case "$inline_verdict" in
present) inline_seen=1 ;;
missing) inline_seen=1; inline_missing=1 ;;
esac
done

# Every channel-pinned first-party stub carries the inline marker → exempt.
if [ "$inline_seen" -eq 1 ] && [ "$inline_missing" -eq 0 ]; then
return 0
fi

# Legacy per-file mechanism — accepted during the transition (#549). A repo
# whose stubs are not yet inline-marked is still compliant if its
# sonar-project.properties carries the narrow per-stub exemption.
local content decoded
content=$(gh_api "repos/$ORG/$repo/contents/sonar-project.properties" --jq '.content' 2>/dev/null || echo "")
# A missing properties file is reported separately by check_sonarcloud.
[ -z "$content" ] && return
if [ -z "$content" ]; then
# No properties file. Flag only when a stub needs but lacks the inline marker
# (a missing properties file itself is reported by check_sonarcloud).
if [ "$inline_missing" -eq 1 ]; then
add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \
"First-party caller stub(s) carry a channel-pinned reusable ref (\`@<name>/stable\`, \`@v1\`/\`@v2\`) without the inline \`# NOSONAR(githubactions:S7637)\` marker, and there is no legacy \`sonar-project.properties\` exemption. SonarCloud will flag them as unpinned actions even though the org exempts them from SHA-pinning. Add the inline marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` entry." \
"standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637"
fi
return 0
fi
decoded=$(echo "$content" | base64 -d 2>/dev/null || echo "")
[ -z "$decoded" ] && return
[ -z "$decoded" ] && return 0

local verdict
verdict=$(classify_sonar_s7637_exemption "$decoded")

case "$verdict" in
missing)
add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \
"\`sonar-project.properties\` has no \`sonar.issue.ignore\` for \`githubactions:S7637\`. SonarCloud will flag first-party reusable-ref caller stubs (\`@<name>/stable\`, \`@v1\`/\`@v2\`) as unpinned actions even though the org exempts them from SHA-pinning. Add the canonical per-stub exemption." \
"standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637"
# Only a real gap when a channel-pinned first-party stub actually lacks the
# inline marker; with no such stub there is nothing for S7637 to fire on.
if [ "$inline_missing" -eq 1 ]; then
add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-missing" "warning" \
"No \`githubactions:S7637\` exemption found. SonarCloud will flag first-party reusable-ref caller stubs (\`@<name>/stable\`, \`@v1\`/\`@v2\`) as unpinned actions even though the org exempts them from SHA-pinning. Add the inline \`# NOSONAR(githubactions:S7637)\` marker to each channel-pinned first-party \`uses:\` line (canonical), or the legacy per-stub \`sonar.issue.ignore\` exemption in \`sonar-project.properties\`." \
"standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637"
fi
;;
Comment thread
don-petry marked this conversation as resolved.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
too-broad)
add_finding "$repo" "ci-workflows" "sonar-s7637-exemption-too-broad" "error" \
"\`sonar-project.properties\` suppresses \`githubactions:S7637\` with a blanket workflow-path \`resourceKey\` (e.g. \`workflows/*.yml\` or \`**\`). This also exempts third-party actions in \`ci.yml\`/\`sonarcloud.yml\` from SHA-pin enforcement. Scope the exemption to the individual caller-stub files instead." \
"\`sonar-project.properties\` suppresses \`githubactions:S7637\` with a blanket workflow-path \`resourceKey\` (e.g. \`workflows/*.yml\` or \`**\`). This also exempts third-party actions in \`ci.yml\`/\`sonarcloud.yml\` from SHA-pin enforcement. Scope the exemption to the individual caller-stub files instead, or migrate to the inline \`# NOSONAR(githubactions:S7637)\` marker." \
"standards/ci-standards.md#sonarcloud-exemption-first-party-reusable-ref-s7637"
;;
esac
Expand Down
2 changes: 1 addition & 1 deletion scripts/deploy-standard-workflows.sh
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ fetch_existing() {
is_already_compliant() {
local existing_content="$1" template="$2" repo="$3"
local expected_uses
expected_uses=$(grep -E '^[[:space:]]*uses:' "$template" | head -1 | sed 's/^[[:space:]]*uses:[[:space:]]*//' | tr -d '\r')
expected_uses=$(grep -E '^[[:space:]]*uses:' "$template" | head -1 | sed 's/^[[:space:]]*uses:[[:space:]]*//' | sed 's/[[:space:]]*#.*//' | tr -d '\r')
[[ -z "$expected_uses" ]] && return 1

local prefix="${expected_uses%@*}" ref_after="${expected_uses##*@}" base
Expand Down
104 changes: 64 additions & 40 deletions standards/ci-standards.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,37 +431,63 @@ The org compliance audit already exempts them via `check_action_pinning`, but
SonarCloud is an **external** gate that doesn't know about that exemption, so it
flags them as high-severity findings and can fail the quality gate.

**Standard:** every SonarCloud-gated repo's `sonar-project.properties` MUST
suppress `githubactions:S7637` **only on the individual first-party caller-stub
workflow files** — each stub gets its own `sonar.issue.ignore.multicriteria`
criterion keyed to that file path. A blanket `resourceKey` such as
`**/.github/workflows/*.yml`, `*.yml`, or `**` is **forbidden**: it would also
drop S7637 on `ci.yml` / `sonarcloud.yml`, leaving third-party actions
un-enforced. Third-party actions must always stay SHA-pinned.

> SonarCloud's `sonar.issue.ignore` matches by file path + rule, not by `uses:`
> ref content — so the exemption is scoped per caller-stub file rather than by
> the ref string itself.

Caller stubs that carry only a first-party reusable ref (and therefore need the
exemption): `agent-shield.yml`, `pr-review-mention.yml`, `pr-auto-review.yml`,
`auto-rebase.yml`, `dependabot-rebase.yml`, `dependabot-automerge.yml`,
`dependency-audit.yml`, `feature-ideation.yml`, `add-to-project.yml`,
`dev-lead.yml`. Copy the canonical block verbatim from this repo's
[`sonar-project.properties`](https://github.com/petry-projects/.github/blob/main/sonar-project.properties);
omit criteria for stubs a given repo does not have.
**Standard (canonical — inline NOSONAR in the stub):** the first-party
channel-pinned `uses:` line in every caller stub MUST carry an inline
`# NOSONAR(githubactions:S7637)` marker, e.g.:

```yaml
jobs:
dev-lead:
# First-party channel tag — do NOT SHA-pin (AGENTS.md mutable-ref exception).
uses: petry-projects/.github-private/.github/workflows/dev-lead-reusable.yml@dev-lead/stable # NOSONAR(githubactions:S7637) first-party channel ref
```

SonarCloud honors the inline comment and suppresses S7637 on exactly that line,
so the exemption travels **with the stub file** — adopters who copy the template
verbatim inherit it with **zero** per-repo `sonar-project.properties` entries and
**no file rename**. Spike-validated on `bmad-bgreat-suite#334` (`3cbfa005`):
Quality Gate ERROR → OK, `new_security_rating` C → A, required `SonarCloud`
check fail → pass.

The marker goes **only on first-party reusable-ref lines** pinned to a moving
channel/tag (`@<name>/stable`, `@v1`/`@v2`). A SHA-pinned ref (e.g.
`feature-ideation.yml`'s `@<sha> # v1`) already satisfies S7637 and gets **no**
marker. Third-party actions stay SHA-pinned and S7637-enforced; never add a
blanket glob or a file-wide disable.

Caller-stub templates carrying a channel-pinned first-party reusable ref (and
therefore the inline marker): `dev-lead.yml`, `agent-shield.yml`,
`pr-review-mention.yml`, `pr-auto-review.yml`, `auto-rebase.yml`,
`dependabot-rebase.yml`, `dependabot-automerge.yml`, `dependency-audit.yml`,
`add-to-project.yml`, `idea-triage.yml`, `idea-enhancer.yml`,
`initiative-planner.yml`. Copy each template verbatim from
[`standards/workflows/`](https://github.com/petry-projects/.github/tree/main/standards/workflows)
and the marker comes with it.

**Legacy (per-file `sonar-project.properties` — optional, transitional):** the
older mechanism suppressed S7637 **only on the individual caller-stub workflow
files** via one `sonar.issue.ignore.multicriteria` criterion per file path. It
still works and is **accepted during the transition**, but is no longer required
once the stubs carry the inline marker — the inline marker is the canonical
mechanism and auto-covers any newly added channel-pinned stub without a per-repo
edit. A blanket `resourceKey` such as `**/.github/workflows/*.yml`, `*.yml`, or
`**` remains **forbidden**: it would also drop S7637 on `ci.yml` /
`sonarcloud.yml`, leaving third-party actions un-enforced.

```properties
# Legacy / optional — only needed for stubs not yet carrying the inline marker.
sonar.issue.ignore.multicriteria=s7637_agentshield,…
sonar.issue.ignore.multicriteria.s7637_agentshield.ruleKey=githubactions:S7637
sonar.issue.ignore.multicriteria.s7637_agentshield.resourceKey=**/agent-shield.yml
# …one ruleKey/resourceKey pair per caller-stub file
```

The compliance audit's `check_sonar_s7637_exemption` enforces this org-wide: it
files `sonar-s7637-exemption-missing` (warning) when a SonarCloud-gated repo's
properties carry no S7637 exemption, and `sonar-s7637-exemption-too-broad`
(error) when the exemption uses a blanket workflow-path `resourceKey`.
The compliance audit's `check_sonar_s7637_exemption` enforces this org-wide: a
repo is compliant when every channel-pinned first-party caller stub carries the
inline marker **or** (during transition) the legacy per-file properties
exemption is present. It files `sonar-s7637-exemption-missing` (warning) when a
SonarCloud-gated repo has neither, and `sonar-s7637-exemption-too-broad` (error)
when the legacy exemption uses a blanket workflow-path `resourceKey`.

### 4. Secret Scanning (`ci.yml` — gitleaks job)

Expand Down Expand Up @@ -1610,26 +1636,24 @@ The cron's three `repository_dispatch` retry types — `dev-lead-ci-failure`,
3. Set `GH_PAT_WORKFLOWS` — a PAT with read access to `petry-projects/.github-private` — as an org or repo secret (required for cross-repo script access).
4. Optionally set `vars.DEV_LEAD_ENGINE` to `claude` (default), `gemini`, or `copilot`.
5. Optionally set `vars.DEV_LEAD_DRY_RUN=true` during the initial rollout period.
6. **If the repo is SonarCloud-gated, add the `s7637_devlead` exemption to
`sonar-project.properties`** (required). `dev-lead.yml` is a first-party
6. **If the repo is SonarCloud-gated**, the S7637 exemption is already baked
into the stub: `dev-lead.yml`'s `uses:` line carries the inline
`# NOSONAR(githubactions:S7637)` marker, so copying the template verbatim
(step 1) is all that is required. `dev-lead.yml` is a first-party
reusable-ref caller stub pinned to a moving channel tag — `@dev-lead/stable`,
or a canary-ring tag (`@dev-lead/ring0` | `@dev-lead/ring1` | `@dev-lead/next`)
under the [canary-rings rollout](#reusable-workflow-versioning--the-stable-channel).
That mutable ref is intentionally **not** SHA-pinned, so SonarCloud's
`githubactions:S7637` flags it at HIGH severity, drives `new_security_rating`
to **C**, and fails the Quality Gate — blocking the required `SonarCloud`
check. Add the per-file criterion (keyed to `**/dev-lead.yml`, **never** a
blanket `resourceKey`) exactly as for every other caller stub:

```properties
sonar.issue.ignore.multicriteria.s7637_devlead.ruleKey=githubactions:S7637
sonar.issue.ignore.multicriteria.s7637_devlead.resourceKey=**/dev-lead.yml
```

and add `s7637_devlead` to the `sonar.issue.ignore.multicriteria` list. See
[SonarCloud Exemption: First-Party Reusable-Ref S7637](#sonarcloud-exemption-first-party-reusable-ref-s7637)
for the canonical block; `check_sonar_s7637_exemption` files
`sonar-s7637-exemption-missing` against repos that omit it.
That mutable ref is intentionally **not** SHA-pinned, so without the marker
SonarCloud's `githubactions:S7637` would flag it at HIGH severity, drive
`new_security_rating` to **C**, and fail the Quality Gate — blocking the
required `SonarCloud` check. The inline marker suppresses S7637 on exactly
that line and travels with the file. **No `sonar-project.properties` entry is
needed.** (The legacy per-file `s7637_devlead` criterion keyed to
`**/dev-lead.yml` still works and is accepted during the transition, but is
optional once the stub carries the marker.) See
[SonarCloud Exemption: First-Party Reusable-Ref S7637](#sonarcloud-exemption-first-party-reusable-ref-s7637);
`check_sonar_s7637_exemption` files `sonar-s7637-exemption-missing` only when
a repo has neither the inline marker nor the legacy entry.

### Required secrets

Expand Down
2 changes: 1 addition & 1 deletion standards/workflows/add-to-project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
add-to-project:
permissions:
contents: read
uses: petry-projects/.github/.github/workflows/add-to-project-reusable.yml@add-to-project/stable
uses: petry-projects/.github/.github/workflows/add-to-project-reusable.yml@add-to-project/stable # NOSONAR(githubactions:S7637) first-party channel ref
with:
project_id: PVT_kwDOD2inqs4BZq3-
project_url: https://github.com/orgs/petry-projects/projects/1
Expand Down
2 changes: 1 addition & 1 deletion standards/workflows/agent-shield.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ permissions:

jobs:
agent-shield:
uses: petry-projects/.github/.github/workflows/agent-shield-reusable.yml@agent-shield/stable
uses: petry-projects/.github/.github/workflows/agent-shield-reusable.yml@agent-shield/stable # NOSONAR(githubactions:S7637) first-party channel ref
2 changes: 1 addition & 1 deletion standards/workflows/auto-rebase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,5 +50,5 @@ jobs:
permissions:
contents: write # update-branch via GITHUB_TOKEN (may touch .github/workflows/)
pull-requests: write # post comments on PRs
uses: petry-projects/.github/.github/workflows/auto-rebase-reusable.yml@auto-rebase/stable
uses: petry-projects/.github/.github/workflows/auto-rebase-reusable.yml@auto-rebase/stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets: inherit
2 changes: 1 addition & 1 deletion standards/workflows/dependabot-automerge.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,5 @@ jobs:
permissions:
contents: read
pull-requests: read
uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@dependabot-automerge/stable
uses: petry-projects/.github/.github/workflows/dependabot-automerge-reusable.yml@dependabot-automerge/stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets: inherit
2 changes: 1 addition & 1 deletion standards/workflows/dependabot-rebase.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
permissions:
contents: write # update-branch via GITHUB_TOKEN (may touch .github/workflows/)
pull-requests: write # re-approve PRs after branch update
uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@dependabot-rebase/stable
uses: petry-projects/.github/.github/workflows/dependabot-rebase-reusable.yml@dependabot-rebase/stable # NOSONAR(githubactions:S7637) first-party channel ref
secrets:
APP_ID: ${{ secrets.APP_ID }}
APP_PRIVATE_KEY: ${{ secrets.APP_PRIVATE_KEY }}
2 changes: 1 addition & 1 deletion standards/workflows/dependency-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ permissions:

jobs:
dependency-audit:
uses: petry-projects/.github/.github/workflows/dependency-audit-reusable.yml@dependency-audit/stable
uses: petry-projects/.github/.github/workflows/dependency-audit-reusable.yml@dependency-audit/stable # NOSONAR(githubactions:S7637) first-party channel ref
Loading
Loading