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
41 changes: 41 additions & 0 deletions .github/workflows/repo-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# repo-release — release-side artifacts (SPDX SBOM → release assets).
#
# Split out of repo-standard.yml (#55): the SBOM upload needs `contents: write`, the
# highest ceiling of the old monolith. Because a caller must grant the UNION of every
# job's permissions at load time, leaving this in repo-standard forced even a read-only
# spell run to grant contents: write — the escalation that made #54 fail to start.
# One reusable workflow, one authority ceiling: a caller grants contents: write here and
# nowhere else, on a dedicated job gated to release/publish (non-PR) events.
name: repo-release

on:
workflow_call:
inputs:
upload-release-assets:
description: "Attach the SBOM to the GitHub Release (needs contents: write). False = artifact only (e.g. self-test / no release)."
type: boolean
default: true

permissions:
contents: read

jobs:
sbom:
# Release-side: runs on push/tags/release, not PR.
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Generate SBOM (SPDX)
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
format: spdx-json
artifact-name: sbom.spdx.json
output-file: sbom.spdx.json
upload-artifact: true
upload-release-assets: ${{ inputs.upload-release-assets }}
50 changes: 50 additions & 0 deletions .github/workflows/repo-scorecard.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# repo-scorecard — OSSF Scorecard supply-chain posture (report-only).
#
# Split out of repo-standard.yml (#55): Scorecard needs `actions: read`, one step above
# the standard `contents: read` floor. Because a reusable workflow's caller must grant
# the UNION of every job's permissions at load time, folding this into repo-standard
# would force ~every repo (scorecard was default-on) to grant actions: read even for a
# read-only spell/security run. Keeping it here — one reusable workflow, one authority
# ceiling — lets a caller grant exactly this ceiling, on a dedicated job.
#
# A caller opts in by adding a job that `uses:` this workflow and grants
# `contents: read` + `actions: read`, gated to non-PR events (Scorecard scores the
# repo, not a diff). See repo-standard-selftest.yml for the reference caller.
name: repo-scorecard

on:
workflow_call: {}

permissions:
contents: read

jobs:
scorecard:
# Scorecard runs on push/schedule, not PR — it scores the repo, not a diff.
if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Report-only: publish_results=false and no SARIF upload (upload needs Advanced
# Security on private repos), so results are kept as an artifact. That keeps the
# perms minimal (contents+actions read) so callers don't need security-events.
- name: OSSF Scorecard
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: false
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: scorecard-results
path: results.sarif
retention-days: 5
57 changes: 57 additions & 0 deletions .github/workflows/repo-standard-selftest.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# repo-standard-selftest — the reference caller, and the guard #55 lacked.
#
# repo-standard.yml (and its siblings) are reusable workflows, but nothing in this repo
# CALLED them — so #54 raised repo-standard's permission floor above contents: read and
# passed this repo's CI, only failing once a downstream caller bumped its pin (#55, #56).
#
# This workflow calls each reusable workflow with exactly its intended authority ceiling.
# GitHub validates the permission union at load time, before `if:`, so a future revision
# that raises a floor fails HERE, pre-merge, instead of in every downstream repo. It is
# also the copy-paste caller template each repo's .github/workflows/standard.yml follows.
name: repo-standard-selftest

on:
pull_request:
paths:
- ".github/workflows/repo-standard.yml"
- ".github/workflows/repo-scorecard.yml"
- ".github/workflows/repo-release.yml"
- ".github/workflows/repo-standard-selftest.yml"
push:
branches: [main]

# No ambient authority — every capability is granted per-job, scoped to the workflow it
# invokes. This is the pattern callers copy.
permissions: {}

jobs:
# Contract: repo-standard.yml starts with ONLY contents: read (the #55 regression).
standard:
uses: ./.github/workflows/repo-standard.yml
permissions:
contents: read
with:
security: true
spell: false # this repo has no cspell.json
test: false
descriptor: false

# Contract: repo-scorecard.yml's ceiling is exactly contents: read + actions: read.
# Gated to non-PR so PR runs never instantiate the elevated grant.
scorecard:
if: ${{ github.event_name != 'pull_request' }}
uses: ./.github/workflows/repo-scorecard.yml
permissions:
contents: read
actions: read

# Contract: repo-release.yml's ceiling is exactly contents: write. upload-release-assets
# is false so the self-test exercises the permission contract without creating a release
# asset on this repo. Gated to non-PR (same as a real release caller).
release:
if: ${{ github.event_name != 'pull_request' }}
uses: ./.github/workflows/repo-release.yml
permissions:
contents: write
with:
upload-release-assets: false
83 changes: 15 additions & 68 deletions .github/workflows/repo-standard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@
# See bounded-systems/conformance for the caller template + rollout.
#
# Behaviors (all opt-in):
# security (default true) — OSV-Scanner (report-only) + dependency-review (PR gate)
# scorecard (default true) — OSSF Scorecard supply-chain posture (report-only)
# sbom (default false) — SPDX SBOM (uploads release assets; needs contents: write)
# spell (default false) — cspell dictionary/allowlist gate (needs a cspell.json)
# test (default false) — set up `runtime` and run `test-command`
# security (default true) — OSV-Scanner (report-only) + dependency-review (PR gate)
# spell (default false) — cspell dictionary/allowlist gate (needs a cspell.json)
# test (default false) — set up `runtime` and run `test-command`
# descriptor (default false) — README managed blocks must match trellis.json
#
# All actions SHA-pinned (org policy). Invocations copied from prx's proven
# osv-scanner.yml / dependency-review.yml / scorecard.yml / sbom.yml, generalized
# to be language-agnostic. scorecard/sbom raise the security floor org-wide — see
# workflow-standardization: they previously ran on ~2 of 72 repos.
# AUTHORITY FLOOR — this workflow is `contents: read` and every job stays within it,
# so a caller only ever needs to grant `contents: read`. GitHub validates a called
# reusable workflow's permission requests as the UNION of all its jobs, at load time,
# before any `if:` — so a single job needing more would force every caller to over-grant
# (that was #55). Jobs that need more authority therefore live in sibling reusable
# workflows, one per ceiling, so a read-only caller is never forced above the floor:
# scorecard → repo-scorecard.yml (adds actions: read)
# sbom → repo-release.yml (adds contents: write)
# Callers opt into those with a dedicated, event-gated job (see #55 / #56).
#
# All actions SHA-pinned (org policy).
name: repo-standard

on:
Expand All @@ -23,14 +29,6 @@ on:
description: "Run OSV-Scanner + dependency-review."
type: boolean
default: true
scorecard:
description: "Run OSSF Scorecard (report-only supply-chain posture)."
type: boolean
default: true
sbom:
description: "Generate an SPDX SBOM (uploads to release assets; caller must grant contents: write)."
type: boolean
default: false
spell:
description: "Spell-gate: fail on tokens not in the dictionaries or the repo's cspell.json words allowlist (needs a cspell.json in the repo)."
type: boolean
Expand Down Expand Up @@ -108,57 +106,6 @@ jobs:
with:
fail-on-severity: high

scorecard:
# OSSF Scorecard runs on push/schedule (not PR — it scores the repo, not a diff).
if: ${{ inputs.scorecard && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
# Report-only: publish_results=false and no SARIF upload (upload needs Advanced
# Security on private repos), so results are kept as an artifact. That keeps the
# perms minimal (contents+actions read) so callers don't need security-events.
- name: OSSF Scorecard
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: false
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: scorecard-results
path: results.sarif
retention-days: 5

sbom:
# Opt-in (default false): uploads to release assets, so the caller must grant
# contents: write. Meant for repos that publish releases.
if: ${{ inputs.sbom && github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Generate SBOM (SPDX)
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
with:
format: spdx-json
artifact-name: sbom.spdx.json
output-file: sbom.spdx.json
upload-artifact: true
upload-release-assets: true

spell:
# Opt-in (default false): fails on any token not in the dictionaries or the
# repo's cspell.json `words` allowlist. Catches nonsense / promotional
Expand Down
Loading