Skip to content

leanprover-community/privilege-escalation-bridge

Repository files navigation

GitHub Actions Privilege Escalation Bridge

This repository provides two TypeScript-based JavaScript actions implementing a fork-safe two-stage GitHub Actions pattern:

  1. Unprivileged workflow (pull_request, issue_comment, pull_request_review, etc.) emits structured data as an artifact.
  2. Privileged workflow (workflow_run) consumes and validates that artifact before doing writes/secrets operations.

Actions

Action Purpose
privilege-escalation-bridge/emit Package outputs, metadata, optional selected event payload, and optional files into a stable artifact schema
privilege-escalation-bridge/consume Download and validate producer artifact from workflow_run, then expose outputs and extracted fields

Security Model

The bridge does not bypass GitHub's permissions model.

Defenses included:

  • Source run binding: meta.workflow_run_id must match expected run.
  • Run-attempt binding: meta.workflow_run_attempt must match expected attempt when available.
  • Repository binding: meta.repository must match current repository.
  • Optional pinning: workflow name, event type, PR number, and head SHA checks.
  • Fail-closed defaults for missing artifact and validation mismatches.

Non-goals:

  • Trusting artifact contents as safe.
  • Passing secrets through artifacts.
  • Preventing logical misuse of untrusted outputs by downstream steps.

Artifact Contract (Schema v2)

Artifact payload layout:

bridge/
  outputs.json
  meta.json
  files/
    ...optional copied files

outputs.json

  • JSON object.
  • Keys must match ^[A-Za-z_][A-Za-z0-9_]*$ in strict mode.
  • Values must be scalars (string, number, boolean, null) in strict mode.

meta.json

Required fields (always provided by emit):

  • schema_version - Internal contract version. Current value is 2; consumers fail if it differs.
  • repository - owner/repo of the producer run.
  • workflow_name - Producer workflow name (GITHUB_WORKFLOW).
  • workflow_run_id - Producer run id as a string.
  • workflow_run_attempt - Producer run attempt as a string.
  • event_name - Producer event name (for example pull_request, issue_comment).
  • head_sha - Producer commit SHA.
  • created_at - ISO timestamp generated by emit.

Optional fields (provided by emit when available):

  • pr_number - Numeric PR/issue number when one exists in the event payload.
  • producer_job - Producer job id (GITHUB_JOB) when available.
  • producer_step - Reserved for compatibility; may be absent.
  • event - Event payload object, controlled by include_event / event_fields.
  • Any additional keys from emit.meta.

Notes:

  • meta is validated strictly on consume for required fields/types and schema version.
  • emit.meta can override default keys if you reuse the same names; avoid overriding core keys unless intentional.
  • event only contains scalar leaves (strings/numbers/booleans/null) when selected via minimal/event_fields.

meta.event Shapes

  • include_event: none - no meta.event key is added.
  • include_event: minimal - a fixed curated subset is included (listed below).
  • include_event: full - entire github.context.payload is included.
  • event_fields present - overrides include_event and includes only the listed paths.

Exact include_event: minimal paths:

  • action
  • sender.login
  • sender.type
  • issue.number
  • issue.title
  • issue.html_url
  • issue.user.login
  • comment.body
  • comment.path
  • comment.user.login
  • review.body
  • review.state
  • review.user.login
  • pull_request.number
  • pull_request.title
  • pull_request.html_url
  • pull_request.user.login
  • pull_request.base.ref
  • pull_request.base.sha
  • pull_request.base.repo.full_name
  • pull_request.head.ref
  • pull_request.head.sha
  • pull_request.head.repo.full_name

Path rules:

  • Paths use dot notation (pull_request.user.login).
  • Multiple paths are comma or newline separated.
  • Missing paths are ignored.
  • Nested objects are reconstructed from the selected scalar leaves.

emit Action

Path: emit/action.yml

Inputs

  • artifact (default: bridge)
    • Artifact name to upload.
  • outputs
    • JSON object string of outputs.
    • Merge precedence: wins over keys from outputs_file when both specify the same key.
  • outputs_file
    • Path to a JSON object file of outputs.
  • files
    • Newline-separated paths copied into bridge/files/.
    • Must be relative paths and may not escape the workspace (.. is rejected).
  • retention_days
    • Artifact retention passed to GitHub artifact upload.
  • sanitize (strict default, or none)
    • strict: enforce output key regex and scalar-only output values.
    • none: skip those checks (consumer still reads with strict output validation).
  • meta
    • Extra metadata JSON object merged into meta.json.
  • include_event (none, minimal, full; default minimal)
    • Controls whether/how meta.event is populated when event_fields is not set.
  • event_fields
    • Optional comma/newline-separated allowlist of event paths.
    • If non-empty, it overrides include_event.

Outputs

  • artifact
  • outputs-json
  • meta-json

consume Action

Path: consume/action.yml

Inputs

  • token (recommended)
    • Token with actions:read for downloading artifacts.
    • No default is set by this action.
    • In reusable workflows, pass explicitly: token: ${{ github.token }}.
  • github_token (deprecated alias for token)
    • Supported for backward compatibility.
  • Env fallback
    • If neither input is set, consume checks GITHUB_TOKEN then GH_TOKEN.
    • Caller env values are not automatically inherited by reusable workflows.
    • Must allow actions:read in the target repository.
  • artifact (default: bridge)
    • Artifact name to download from the producer run.
  • override_json
    • Optional JSON object with canonical bridge payload fields meta and outputs.
    • When non-empty, consume skips token resolution and artifact download and uses this payload instead.
    • meta must satisfy the normal bridge metadata schema; outputs must be a JSON object with scalar values.
    • bridge/files restore is not supported in this mode, so files-path is not emitted.
  • run_id (defaults to triggering workflow_run.id)
    • Required unless the action is running under a workflow_run event.
    • In override_json mode, this binding is only checked when a run id is available from input or event context.
  • source_workflow
    • Optional exact match against meta.workflow_name.
  • expected_head_sha
    • Optional exact match against meta.head_sha.
    • Usually not needed for workflow_run consumers because run_id (and run_attempt when present) are already validated by default.
    • For pull_request producers, this is often the synthetic merge commit SHA (refs/pull/<n>/merge), which changes when the base branch moves and can make this check brittle.
  • expected_pr_number
    • Optional exact match against meta.pr_number.
  • require_event (comma/newline-separated event names)
    • Allowlist for meta.event_name.
  • fail_on_missing (default: true)
    • true: missing artifact fails the action.
    • false: missing artifact does not fail; JSON outputs are emitted as {} and no per-key outputs are emitted.
  • expose (outputs, env, both; default outputs)
    • Controls where per-key bridge outputs and extracted mappings are written.
    • outputs: step outputs only.
    • env: exported environment variables only.
    • both: both outputs and env vars.
  • prefix prefix for per-key bridge outputs
    • Applied only to direct bridge output keys from outputs.json.
    • Not applied to names from extract mappings.
  • extract newline-separated mappings: NAME=source.path
    • Supported roots: outputs, meta, event.
    • Every configured mapping must resolve to a scalar value or null; missing paths fail the action.
    • Fallback paths are supported: a.b|c.d (first found wins).
  • path restore destination for bridge/files (default .bridge)
    • Files are copied into this directory recursively.

Outputs

  • Per-key outputs (subject to expose and prefix)
  • Extracted outputs from extract mappings
  • outputs-json
  • meta-json
  • event-json
  • files-path when artifact-backed restore ran

When fail_on_missing=false and the artifact is not found, outputs-json is {}. You can skip downstream work with a guard like if: ${{ steps.bridge.outputs.outputs-json != '{}' }}.

Validation Checks Performed by consume

consume always validates:

  • meta.repository matches current repository.
  • meta.workflow_run_id matches requested run_id.
  • meta.workflow_run_attempt matches triggering workflow_run.run_attempt when available.
    • These checks are typically sufficient to bind the artifact to the triggering workflow run.

consume optionally validates when inputs are provided:

  • source_workflow -> meta.workflow_name
  • expected_head_sha -> meta.head_sha
  • expected_pr_number -> meta.pr_number
  • require_event -> meta.event_name membership

Logging

Both actions use collapsible log groups in the Actions UI for major phases.

  • Standard runs show concise core.info summaries.
  • Detailed internals are emitted via core.debug and appear when step debug logging is enabled (ACTIONS_STEP_DEBUG=true).

Quick Example

Unprivileged Producer

on: pull_request

jobs:
  checks:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@v4

      - run: echo '{"lint_ok":true}' > bridge.json
      - run: echo '{"ok":true}' > lint.json

      - uses: leanprover-community/privilege-escalation-bridge/emit@v1
        with:
          artifact: pr-bridge
          outputs_file: bridge.json
          include_event: minimal
          files: |
            lint.json

Privileged Consumer

on:
  workflow_run:
    workflows: ["PR Checks"]
    types: [completed]

jobs:
  consume:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    permissions:
      actions: read
      contents: write
      pull-requests: write
    steps:
      - id: bridge
        uses: leanprover-community/privilege-escalation-bridge/consume@v1
        with:
          token: ${{ github.token }}
          artifact: pr-bridge
          source_workflow: PR Checks
          expected_head_sha: ${{ github.event.workflow_run.head_sha }}
          require_event: pull_request
          extract: |
            pr_number=meta.pr_number
            author=event.pull_request.user.login

      - run: echo "pr=${{ steps.bridge.outputs.pr_number }} author=${{ steps.bridge.outputs.author }}"

Development

Install

npm ci

Test

npm test

Build

npm run build

License

Apache-2.0

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors