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
2 changes: 2 additions & 0 deletions .github/workflows/actions.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ workflows:
'.github/workflows/affinescript-verify.yml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
- 'ocaml/setup-ocaml@605a7e998e76e035b82c14d618a6e1010732c4ce'
'.github/workflows/allowlist-preflight-reusable.yml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
'.github/workflows/boj-build.yml':
- 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1'
'.github/workflows/casket-pages.yml':
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/allowlist-preflight-reusable.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# This workflow is managed by gh actions-lock.
# SPDX-License-Identifier: MPL-2.0
# Reusable fail-closed Actions policy preflight (standards#486).
name: Actions Policy Preflight Reusable

on:
workflow_call:
secrets:
HYPATIA_SCAN_PAT:
description: Fine-grained token with repository Administration read access
required: false

permissions:
contents: read

jobs:
preflight:
name: Actions policy (tree + live)
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: Checkout caller repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.repository }}
ref: ${{ github.sha }}

- name: Checkout policy source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: hyperpolymath/standards
ref: main
path: .standards-checkout
sparse-checkout: |
rhodium-standard-repositories/actions-allowlist
scripts/check-actions-policy.sh
scripts/check-allowed-actions.sh
sparse-checkout-cone-mode: false

- name: Stage policy tools outside the scanned tree
run: |
if [ "$GITHUB_REPOSITORY" = hyperpolymath/standards ]; then
cp scripts/check-actions-policy.sh scripts/check-allowed-actions.sh "$RUNNER_TEMP/"
cp rhodium-standard-repositories/actions-allowlist/allowed-actions.json \
"$RUNNER_TEMP/allowed-actions.json"
else
cp .standards-checkout/scripts/check-actions-policy.sh \
.standards-checkout/scripts/check-allowed-actions.sh "$RUNNER_TEMP/"
cp .standards-checkout/rhodium-standard-repositories/actions-allowlist/allowed-actions.json \
"$RUNNER_TEMP/allowed-actions.json"
fi
rm -rf .standards-checkout

- name: Check workflow references against canonical allowlist
run: |
bash "$RUNNER_TEMP/check-allowed-actions.sh" \
"$RUNNER_TEMP/allowed-actions.json" .github/workflows

- name: Check live repository policy
env:
GH_TOKEN: ${{ secrets.HYPATIA_SCAN_PAT || github.token }}
run: |
bash "$RUNNER_TEMP/check-actions-policy.sh" \
"$GITHUB_REPOSITORY" "$RUNNER_TEMP/allowed-actions.json"
41 changes: 29 additions & 12 deletions .github/workflows/governance-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ on:
type: string
required: false
default: ubuntu-latest
secrets:
HYPATIA_SCAN_PAT:
description: Fine-grained token with repository Administration read access
required: false

permissions:
actions: read
Expand Down Expand Up @@ -83,20 +87,33 @@ jobs:
scripts
sparse-checkout-cone-mode: false

- name: Run allowlist preflight check
- name: Run tree allowlist preflight check
run: |
# Bootstrap rule: standards must test the scripts from its PR tree;
# consumers use the canonical copies checked out from standards.
if [ "$GITHUB_REPOSITORY" = hyperpolymath/standards ]; then
cp scripts/check-actions-policy.sh scripts/check-allowed-actions.sh "$RUNNER_TEMP/"
cp rhodium-standard-repositories/actions-allowlist/allowed-actions.json \
"$RUNNER_TEMP/allowed-actions.json"
else
cp .standards-checkout/scripts/check-actions-policy.sh \
.standards-checkout/scripts/check-allowed-actions.sh "$RUNNER_TEMP/"
cp .standards-checkout/rhodium-standard-repositories/actions-allowlist/allowed-actions.json \
"$RUNNER_TEMP/allowed-actions.json"
fi
bash "$RUNNER_TEMP/check-allowed-actions.sh" \
"$RUNNER_TEMP/allowed-actions.json" .github/workflows

- name: Check live Actions policy
env:
# GitHub's job token cannot read this Administration endpoint. Keep
# this dependency explicit: missing/expired credentials must make the
# live control red rather than silently reducing it to a tree check.
GH_TOKEN: ${{ secrets.HYPATIA_SCAN_PAT }}
run: |
# check-actions-policy.sh `exec`s its SIBLING check-allowed-actions.sh
# via "${0%/*}/...". Copying only the first script and then deleting
# the checkout left that sibling missing, so the step died with exit
# 127 (command not found) on every run. Stage both, plus the canonical
# allowlist itself — consumer repos have no copy of it in their tree.
cp .standards-checkout/scripts/check-actions-policy.sh \
.standards-checkout/scripts/check-allowed-actions.sh "$RUNNER_TEMP/"
cp .standards-checkout/rhodium-standard-repositories/actions-allowlist/allowed-actions.json \
"$RUNNER_TEMP/allowed-actions.json"
rm -rf .standards-checkout
ALLOWLIST_JSON="$RUNNER_TEMP/allowed-actions.json" \
bash "$RUNNER_TEMP/check-actions-policy.sh" .github/workflows
bash "$RUNNER_TEMP/check-actions-policy.sh" \
"$GITHUB_REPOSITORY" "$RUNNER_TEMP/allowed-actions.json"

validate-hypatia-baseline:
name: Validate Hypatia Baseline
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/governance.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ permissions:
jobs:
governance:
uses: ./.github/workflows/governance-reusable.yml
secrets: inherit
2 changes: 1 addition & 1 deletion .machine_readable/REGISTRY.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ name = "RSR — Rhodium Standard Repositories"
stream = "governance"
home = "rhodium-standard-repositories/"
canonical_doc = "rhodium-standard-repositories/README.adoc"
source_hash = "sha256:8b6478c4900181fa5ab17bccaf9dafe2c8e5d1c22bd03d7c3a7a3feaf048cb8c"
source_hash = "sha256:4e8ff4f5ff07dece3ecd714221e84f2354137e0ded7c291d71c5edac529e696e"
route = "the repository-compliance standard every repo is graded against"

[[spec]]
Expand Down
14 changes: 14 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,20 @@ staleness-test:
@echo "=== propagate-workflow-pins ==="
@bash scripts/tests/propagate-workflow-pins-test.sh

# Check the repository's workflow references and, when a repository slug is
# supplied, its live Actions policy. Example: just check-allowlist hyperpolymath/standards
check-allowlist repository="":
@bash scripts/check-allowed-actions.sh rhodium-standard-repositories/actions-allowlist/allowed-actions.json .github/workflows
@if [ -n "{{repository}}" ]; then bash scripts/check-actions-policy.sh "{{repository}}"; fi

# Apply the estate default (all + mandatory SHA pinning). Use posture=selected
# only for a designated high-sensitivity repository.
set-allowlist repository posture="all":
@ACTIONS_POSTURE="{{posture}}" bash scripts/set-allowed-actions.sh "{{repository}}"

actions-policy-test:
@bash scripts/tests/actions-policy-486-test.sh

# Wave-0 anti-false-green regression: proves each fixed validator CAN fail
false-green-test:
@bash scripts/tests/wave0-false-green-test.sh
Expand Down
40 changes: 26 additions & 14 deletions rhodium-standard-repositories/actions-allowlist/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ every repo's live allowlist in sync with it.

== The failure this prevents

RSR repos run with `allowed_actions: selected` + `sha_pinning_required: true`
(a security posture). Under that setting, if a workflow `uses:` an action or
RSR repos default to `allowed_actions: all` + `sha_pinning_required: true`.
The mandatory SHA setting keeps action references immutable while avoiding an
empty-list outage by construction. Designated release/signing repositories use
the stricter `selected` posture. Under that posture, if a workflow `uses:` an action or
reusable-workflow that is **not on the allowlist**, GitHub kills the run at
*startup* — **zero jobs, no log, an opaque "workflow file issue"**. You cannot
"squabble" a run that never started: there is no gate to satisfy, no log to read.
Expand All @@ -29,7 +31,7 @@ an explicit `patterns_allowed` list). This is the single list every repo's
`selected-actions` policy must be set from. Add a pattern here when the estate
adopts a new action; the check below then enforces it everywhere.

== The three tools
== The policy tools

[cols="1,2,3",options="header"]
|===
Expand All @@ -39,17 +41,23 @@ adopts a new action; the check below then enforces it everywhere.
| `scripts/check-allowed-actions.sh`
| Turns the opaque `startup_failure` into a *legible, satisfiable* gate: lists exactly which `uses:` are not covered. No API, no admin — run it in a preflight job, in the governance gate, or locally. Exit 1 on any gap.

| **Live-policy gate**
| `scripts/check-actions-policy.sh`
| Reads the repository's actual Actions settings. It blocks disabled Actions, SHA pinning being off, an empty selected allowlist, or a selected list that is not a superset of the canonical policy.

| **Hypatia** (scanner)
| `hypatia-rules/workflow-allowlist-gap.a2ml` (HYP-S008)
| Scans the workflow tree and emits a `compliance.finding.new` for each uncovered `uses:` — so the gap is caught at scan time, before a run ever startup-fails.

| **Robot-repo-automaton** (onboarding)
| `scripts/set-allowed-actions.sh`
| The ROOT prevention: applies the canonical allowlist. The farm/onboarding process MUST run this (with an admin PAT) whenever a repo is created or migrated, so `patterns_allowed` is never empty. It **escalates repo → org → enterprise**: a repo-level PUT that returns 409 "already set at the organization or enterprise level" falls back to the org (owner), then the enterprise — because the allowlist is often enforced one or two levels up (as it is for `metadatastician`, governed at the org level). Rhodibot (only `contents`/`pull-requests: write`) cannot set Actions policy, so it *detects + reports* via the check; the admin PAT *sets* it.
| The ROOT prevention: applies `allowed_actions=all` plus mandatory SHA pinning by default, or the canonical selected list when `ACTIONS_POSTURE=selected`. Every PUT is followed by a GET verification that refuses success if GitHub reset SHA pinning. It **escalates repo → org → enterprise** when policy is governed higher up.
|===

An **allowlist preflight** workflow (`.github/workflows/allowlist-preflight.yml`,
seeded from the RSR template) makes this a first-class red check: it uses only
The reusable **allowlist preflight** workflow
(`.github/workflows/allowlist-preflight-reusable.yml`) makes this a first-class
red check: callers pass `secrets: inherit` (or explicitly map
`HYPATIA_SCAN_PAT`) so the live check has Administration-read access. It uses only
`actions/checkout` (github-owned, always permitted) so it can never itself
startup-fail, and runs the check on every push/PR — turning "the whole CI went
dark" into "one legible red check that names the offending `uses:`".
Expand All @@ -58,10 +66,11 @@ dark" into "one legible red check that names the offending `uses:`".

A conforming RSR repo MUST:

. set `allowed_actions = selected` and `sha_pinning_required = true`;
. have a `selected-actions` allowlist that is a superset of what its workflows
`uses:` (verify: `check-allowed-actions.sh` exits 0);
. have that allowlist applied by the onboarding automaton, not hand-curated per repo.
. set `allowed_actions = all` and `sha_pinning_required = true`, or use the
designated high-sensitivity `selected` posture;
. when selected, have a live allowlist that is a superset of the canonical list;
. pass both `check-allowed-actions.sh` (tree) and `check-actions-policy.sh` (live);
. have the policy applied by the onboarding automaton, not hand-curated per repo.

== Applying / fixing a repo

Expand All @@ -70,15 +79,18 @@ A conforming RSR repo MUST:
# Detect gaps (safe, no admin):
bash scripts/check-allowed-actions.sh rhodium-standard-repositories/actions-allowlist/allowed-actions.json .github/workflows

# Apply the canonical allowlist (needs an admin PAT — farm admin token):
# Apply the default all + SHA-pinning policy (needs an admin PAT):
bash scripts/set-allowed-actions.sh <owner>/<repo>

# Designated high-sensitivity repository:
ACTIONS_POSTURE=selected bash scripts/set-allowed-actions.sh <owner>/<repo>
----

Or directly:

[source,console]
----
gh api -X PUT repos/<owner>/<repo>/actions/permissions -F enabled=true -f allowed_actions=selected
gh api -X PUT repos/<owner>/<repo>/actions/permissions/selected-actions \
--input rhodium-standard-repositories/actions-allowlist/allowed-actions.json
gh api -X PUT repos/<owner>/<repo>/actions/permissions \
-F enabled=true -f allowed_actions=all -F sha_pinning_required=true
gh api repos/<owner>/<repo>/actions/permissions
----
88 changes: 58 additions & 30 deletions scripts/check-actions-policy.sh
Original file line number Diff line number Diff line change
@@ -1,38 +1,66 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MPL-2.0
# check-actions-policy.sh — RSR Actions Allowlist Policy gate
# Check the LIVE GitHub Actions policy for a repository (standards#486).
#
# Part of Issue #486: Wire allowlist preflight into governance gate.
#
# This script wraps check-allowed-actions.sh to validate that every `uses:`
# in the caller repo's workflows is covered by the canonical allowlist.
# It is designed to run in CI (as part of governance-reusable.yml) with
# minimal dependencies: only actions/checkout (github-owned, always permitted),
# so it can never itself startup-fail due to an allowlist gap.
#
# Exit codes:
# 0 = all `uses:` are covered by the canonical allowlist
# 1 = one or more gaps detected
# 2 = allowlist file not found (should never happen in standards repo)
#
# Usage: check-actions-policy.sh [WORKFLOWS_DIR]
# WORKFLOWS_DIR defaults to .github/workflows
# Usage: check-actions-policy.sh <owner/repo> [allowed-actions.json]
# Exit: 0 compliant | 1 policy violation/API failure | 2 setup error
set -euo pipefail

WF_DIR="${1:-.github/workflows}"
REPOSITORY="${1:?usage: check-actions-policy.sh <owner/repo> [allowed-actions.json]}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CANON="${2:-${ALLOWLIST_JSON:-$SCRIPT_DIR/../rhodium-standard-repositories/actions-allowlist/allowed-actions.json}}"
GH_BIN="${GH_BIN:-gh}"

[ -f "$CANON" ] || { echo "ERROR: canonical allowlist not found: $CANON" >&2; exit 2; }

Check failure on line 14 in scripts/check-actions-policy.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD2b_t0cXg66ELaq&open=AaAztD2b_t0cXg66ELaq&pullRequest=622
command -v "$GH_BIN" >/dev/null 2>&1 || { echo "ERROR: GitHub CLI not found: $GH_BIN" >&2; exit 2; }

permissions="$($GH_BIN api "repos/$REPOSITORY/actions/permissions")" || {
echo "ERROR: could not read live Actions permissions for $REPOSITORY" >&2
exit 1
}

# The canonical allowlist lives in this repo at
# rhodium-standard-repositories/actions-allowlist/allowed-actions.json
# Resolve the canonical allowlist. $ALLOWLIST_JSON lets the caller point at a
# copy staged outside the scanned tree — required for consumer repos, which do
# not have rhodium-standard-repositories/ in their own checkout.
CANON="${ALLOWLIST_JSON:-rhodium-standard-repositories/actions-allowlist/allowed-actions.json}"
enabled="$(jq -r '.enabled // false' <<<"$permissions")"
allowed="$(jq -r '.allowed_actions // "missing"' <<<"$permissions")"
sha_pin="$(jq -r '.sha_pinning_required // false' <<<"$permissions")"

if [ ! -f "$CANON" ]; then
echo "::error::Canonical allowlist not found: $CANON"
echo "This script must be run from the standards repository or a checkout that includes it."
exit 2
fi
[ "$enabled" = true ] || { echo "ERROR: Actions are disabled for $REPOSITORY" >&2; exit 1; }

Check failure on line 26 in scripts/check-actions-policy.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD2b_t0cXg66ELar&open=AaAztD2b_t0cXg66ELar&pullRequest=622
[ "$sha_pin" = true ] || { echo "ERROR: sha_pinning_required is OFF for $REPOSITORY" >&2; exit 1; }

Check failure on line 27 in scripts/check-actions-policy.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD2b_t0cXg66ELas&open=AaAztD2b_t0cXg66ELas&pullRequest=622

# Run the actual check
exec bash "${0%/*}/check-allowed-actions.sh" "$CANON" "$WF_DIR"
case "$allowed" in
all)
echo "policy ok: repository=$REPOSITORY allowed=all sha_pin=true"
;;
selected)
selected="$($GH_BIN api "repos/$REPOSITORY/actions/permissions/selected-actions")" || {
echo "ERROR: could not read selected Actions policy for $REPOSITORY" >&2
exit 1
}
count="$(jq -r '(.patterns_allowed // []) | length' <<<"$selected")"
[ "$count" -gt 0 ] || {

Check failure on line 39 in scripts/check-actions-policy.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD2b_t0cXg66ELat&open=AaAztD2b_t0cXg66ELat&pullRequest=622
echo "ERROR: allowed_actions=selected with EMPTY patterns_allowed for $REPOSITORY" >&2
exit 1
}
missing="$(jq -n --argjson canon "$(<"$CANON")" --argjson live "$selected" '
[($canon.patterns_allowed // [])[] as $required
| select((($live.patterns_allowed // []) | index($required)) == null)
| $required]')"
if [ "$(jq 'length' <<<"$missing")" -ne 0 ]; then

Check failure on line 47 in scripts/check-actions-policy.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD2b_t0cXg66ELau&open=AaAztD2b_t0cXg66ELau&pullRequest=622
echo "ERROR: live selected allowlist is not a superset of the canonical policy:" >&2
jq -r '.[] | " missing: \(.)"' <<<"$missing" >&2
exit 1
fi
for flag in github_owned_allowed verified_allowed; do
required="$(jq -r --arg flag "$flag" '.[$flag] // false' "$CANON")"
live="$(jq -r --arg flag "$flag" '.[$flag] // false' <<<"$selected")"
if [ "$required" = true ] && [ "$live" != true ]; then

Check failure on line 55 in scripts/check-actions-policy.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD2b_t0cXg66ELav&open=AaAztD2b_t0cXg66ELav&pullRequest=622

Check failure on line 55 in scripts/check-actions-policy.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD2b_t0cXg66ELaw&open=AaAztD2b_t0cXg66ELaw&pullRequest=622
echo "ERROR: selected policy disables required flag $flag" >&2
exit 1
fi
done
echo "policy ok: repository=$REPOSITORY allowed=selected patterns=$count sha_pin=true"
;;
*)
echo "ERROR: unsupported allowed_actions value '$allowed' for $REPOSITORY" >&2
exit 1
;;
esac
14 changes: 11 additions & 3 deletions scripts/check-rsr-profile.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
# VESTIGIAL — present but the repo does not declare the gating capability.
# MISSING — the capability is declared but the module is absent.
#
# Usage: check-rsr-profile.sh [REPO_DIR] (default: current directory)
# Usage: check-rsr-profile.sh [REPO_DIR] [owner/repo]
# Set RSR_REPOSITORY instead of the second argument to enforce the live
# Actions policy alongside the local capability profile.
# Exit: 0 OK | 1 violations | 2 setup error
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
GATES="${RSR_GATES:-$SCRIPT_DIR/../.machine_readable/template-capability-gates.toml}"
REPO="${1:-.}"
LIVE_REPOSITORY="${2:-${RSR_REPOSITORY:-}}"
PROFILE="$REPO/.machine_readable/rsr-profile.a2ml"

[ -f "$GATES" ] || { echo "ERROR: gates file not found: $GATES" >&2; exit 2; }
Expand Down Expand Up @@ -74,14 +77,14 @@
local key="$1"
case "$key" in
*/) [ -d "$REPO/${key%/}" ] ;;
*'*'*) ( shopt -s globstar nullglob; local m=("$REPO"/$key); [ ${#m[@]} -gt 0 ] ) ;;
*'*'*) ( shopt -s globstar nullglob; compgen -G "$REPO/$key" >/dev/null ) ;;
*) [ -e "$REPO/$key" ] ;;
esac
}

echo "repo: $REPO"
echo "profile: ${PRESET:+preset=$PRESET }${DIRECT:+direct-capabilities}"
echo "effective capabilities: $(printf '%s ' $EFFECTIVE)"
echo "effective capabilities: $(printf '%s\n' "$EFFECTIVE" | paste -sd ' ' -)"
echo

fail=0
Expand All @@ -108,4 +111,9 @@
MSG
exit 1
fi

if [ -n "$LIVE_REPOSITORY" ]; then

Check failure on line 115 in scripts/check-rsr-profile.sh

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use '[[' instead of '[' for conditional tests. The '[[' construct is safer and more feature-rich.

See more on https://sonarcloud.io/project/issues?id=hyperpolymath_standards&issues=AaAztD3j_t0cXg66ELbA&open=AaAztD3j_t0cXg66ELbA&pullRequest=622
echo "checking live Actions policy: $LIVE_REPOSITORY"
bash "$SCRIPT_DIR/check-actions-policy.sh" "$LIVE_REPOSITORY"
fi
echo "rsr-profile check: OK — scaffold matches declared capabilities."
Loading
Loading