-
Notifications
You must be signed in to change notification settings - Fork 1
Pin workflow dependencies and define release trust boundary #295
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
14fd318
Require human review for execution surfaces
justin808 471a57f
Harden security gate failure handling
justin808 ec651cc
Fail closed for shared pull request heads
justin808 7d77c64
Move human review to stable release promotion
justin808 14ef8ed
Bind upgrades to the reviewed source
justin808 196d6b8
Require a clean reviewed upgrade source
justin808 2bd4fca
Reject unreviewed local upgrade commits
justin808 1c25bfb
Bind review flow to one fetched commit
justin808 085668e
Review complete trees and force copy installs
justin808 a19622e
Remove unsafe review recipe and parse workflow YAML
justin808 79d4293
Align security messaging with release boundary
justin808 5f797ea
Scope action pin checks to workflow uses
justin808 66ac731
Scan composite actions for mutable dependencies
justin808 c9b71ad
Allow local actions and scan their metadata
justin808 7f76994
Accept digest-pinned Docker actions
justin808 c62f6de
Address security policy review feedback
justin808 c202c70
Tighten action reference validation
justin808 9a4b784
Merge remote-tracking branch 'origin/main' into jg-codex/require-huma…
justin808 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| # Executable helpers and agent instructions are a security boundary. Require an | ||
| # administrator code owner in addition to the exact-head human review gate. | ||
| /AGENTS.md @shakacode/admins | ||
| /.agents/ @shakacode/admins | ||
| /.claude-plugin/ @shakacode/admins | ||
| /.codex-plugin/ @shakacode/admins | ||
| /.github/ @shakacode/admins | ||
| /bin/ @shakacode/admins | ||
| /skills/ @shakacode/admins | ||
| /workflows/ @shakacode/admins | ||
| **/*.sh @shakacode/admins | ||
| **/*.bash @shakacode/admins |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| version: 2 | ||
| updates: | ||
| - package-ecosystem: github-actions | ||
| directory: "/" | ||
| schedule: | ||
| interval: weekly | ||
| open-pull-requests-limit: 5 | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| name: Human Security Review | ||
|
|
||
| on: | ||
| pull_request_target: | ||
| types: [opened, reopened, synchronize, ready_for_review] | ||
| # Review events from public forks deliberately receive a read-only token. | ||
| # Re-evaluate open PRs from trusted default-branch code instead. | ||
| schedule: | ||
| - cron: "*/5 * * * *" | ||
| workflow_dispatch: | ||
|
|
||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| statuses: write | ||
|
|
||
| concurrency: | ||
| group: human-security-review | ||
| cancel-in-progress: false | ||
|
|
||
| jobs: | ||
| gate: | ||
| name: Exact-head human approval | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| # The helper and workflow come from the trusted base. Never check out or | ||
| # execute the pull request branch in this privileged event context. | ||
| - name: Checkout trusted base | ||
| uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6 | ||
| with: | ||
| ref: ${{ github.event.pull_request.base.sha || github.sha }} | ||
| persist-credentials: false | ||
|
|
||
| - name: Require current-head human approval for execution surfaces | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| EVENT_HEAD_SHA: ${{ github.event.pull_request.head.sha }} | ||
| EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| pairs="$RUNNER_TEMP/human-security-review-pairs" | ||
| if [[ -n "$EVENT_PR_NUMBER" ]]; then | ||
| printf '%s %s\n' "$EVENT_PR_NUMBER" "$EVENT_HEAD_SHA" > "$pairs" | ||
| else | ||
| gh api --paginate \ | ||
| "repos/$GITHUB_REPOSITORY/pulls?state=open&per_page=100" \ | ||
| --jq '.[] | "\(.number) \(.head.sha)"' > "$pairs" | ||
| fi | ||
|
|
||
| infrastructure_error=0 | ||
| while read -r PR_NUMBER HEAD_SHA; do | ||
| [[ -z "$PR_NUMBER" ]] && continue | ||
| if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]] || [[ ! "$HEAD_SHA" =~ ^[0-9a-f]{40}$ ]]; then | ||
| echo "Invalid pull request identity" >&2 | ||
| infrastructure_error=1 | ||
| continue | ||
| fi | ||
|
|
||
| set +e | ||
| bin/human-security-review-gate \ | ||
| --repo "$GITHUB_REPOSITORY" \ | ||
| --pr "$PR_NUMBER" \ | ||
| --expected-head "$HEAD_SHA" \ | ||
| >"$RUNNER_TEMP/human-security-review-$PR_NUMBER.out" 2>&1 | ||
| gate_status=$? | ||
| set -e | ||
| cat "$RUNNER_TEMP/human-security-review-$PR_NUMBER.out" | ||
|
|
||
| case "$gate_status" in | ||
| 0) | ||
| state=success | ||
| description="Current-head human review satisfied or not required" | ||
| ;; | ||
| 1) | ||
| state=failure | ||
| description="Current-head human approval is required" | ||
| ;; | ||
| *) | ||
| state=error | ||
| description="Human security review could not be verified" | ||
| infrastructure_error=1 | ||
| ;; | ||
| esac | ||
|
|
||
| gh api --method POST \ | ||
| "repos/$GITHUB_REPOSITORY/statuses/$HEAD_SHA" \ | ||
| -f state="$state" \ | ||
| -f context=human-security-review/exact-head \ | ||
|
justin808 marked this conversation as resolved.
Outdated
|
||
| -f description="$description" \ | ||
| -f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" | ||
| done < "$pairs" | ||
|
|
||
| exit "$infrastructure_error" | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| #!/usr/bin/env ruby | ||
| # frozen_string_literal: true | ||
|
|
||
| require "json" | ||
| require "open3" | ||
| require "optparse" | ||
|
|
||
| module HumanSecurityReviewGate | ||
| class InfrastructureError < StandardError; end | ||
|
|
||
| HIGH_RISK_PATHS = [ | ||
| %r{\A(?:\.agents|\.claude-plugin|\.codex-plugin)/}, | ||
| %r{\A\.github/}, | ||
| /\AAGENTS\.md\z/, | ||
| %r{\Abin/}, | ||
| %r{\Askills/}, | ||
| %r{\Aworkflows/}, | ||
| /\.(?:bash|sh)\z/ | ||
| ].freeze | ||
| REVIEW_PERMISSIONS = %w[admin maintain write].freeze | ||
| DECISIVE_REVIEW_STATES = %w[APPROVED CHANGES_REQUESTED DISMISSED].freeze | ||
| PAGE_SIZE = 100 | ||
| MAX_PAGES = 30 | ||
|
|
||
| module_function | ||
|
|
||
| def gh_json(executable, endpoint) | ||
| stdout, stderr, status = Open3.capture3(executable, "api", endpoint) | ||
| return JSON.parse(stdout) if status.success? | ||
|
|
||
| raise InfrastructureError, "GitHub API failed for #{endpoint}: #{stderr.strip}" | ||
| rescue JSON::ParserError => e | ||
| raise InfrastructureError, "invalid GitHub API response for #{endpoint}: #{e.message}" | ||
| end | ||
|
|
||
| def high_risk_path?(path) | ||
| HIGH_RISK_PATHS.any? { |pattern| pattern.match?(path) } | ||
| end | ||
|
|
||
| def gh_paginated_json(executable, endpoint) | ||
| records = [] | ||
| 1.upto(MAX_PAGES) do |page| | ||
| separator = endpoint.include?("?") ? "&" : "?" | ||
| batch = gh_json(executable, "#{endpoint}#{separator}per_page=#{PAGE_SIZE}&page=#{page}") | ||
| raise InfrastructureError, "expected an array from #{endpoint}" unless batch.is_a?(Array) | ||
|
|
||
| records.concat(batch) | ||
| return records if batch.length < PAGE_SIZE | ||
| end | ||
|
|
||
| raise InfrastructureError, "GitHub API coverage exceeded #{MAX_PAGES} pages for #{endpoint}" | ||
| end | ||
|
|
||
| def run(argv) | ||
| options = {} | ||
| OptionParser.new do |parser| | ||
| parser.on("--repo OWNER/REPO") { |value| options[:repo] = value } | ||
| parser.on("--pr NUMBER", Integer) { |value| options[:pr] = value } | ||
| parser.on("--expected-head SHA") { |value| options[:expected_head] = value } | ||
| end.parse!(argv) | ||
|
|
||
| repo = options[:repo] | ||
| number = options[:pr] | ||
| raise OptionParser::MissingArgument, "--repo" if repo.nil? | ||
| raise OptionParser::MissingArgument, "--pr" if number.nil? | ||
| raise OptionParser::InvalidArgument, "--repo must be OWNER/REPO" unless repo.match?(%r{\A[^/\s]+/[^/\s]+\z}) | ||
| raise OptionParser::InvalidArgument, "--pr must be positive" unless number.positive? | ||
|
|
||
| gh = ENV.fetch("AGENT_WORKFLOWS_GH_EXECUTABLE", "gh") | ||
| pull_request = gh_json(gh, "repos/#{repo}/pulls/#{number}") | ||
| head_sha = pull_request.dig("head", "sha") | ||
| author = pull_request.dig("user", "login") | ||
| unless head_sha.is_a?(String) && head_sha.match?(/\A[0-9a-f]{40}\z/) && | ||
| author.is_a?(String) && !author.empty? | ||
| raise InfrastructureError, "pull request identity is missing or malformed" | ||
| end | ||
|
|
||
| expected_head = options[:expected_head] | ||
| if expected_head && !expected_head.match?(/\A[0-9a-f]{40}\z/) | ||
| raise OptionParser::InvalidArgument, "--expected-head must be a full commit SHA" | ||
| end | ||
|
|
||
| if expected_head && expected_head != head_sha | ||
| raise InfrastructureError, "pull request head changed: expected #{expected_head}, found #{head_sha}" | ||
| end | ||
|
|
||
| files = gh_paginated_json(gh, "repos/#{repo}/pulls/#{number}/files") | ||
| changed_paths = files.flat_map do |file| | ||
| [file.fetch("filename"), file["previous_filename"]] | ||
| end | ||
| risky_paths = changed_paths.compact.select { |path| high_risk_path?(path) }.uniq.sort | ||
|
|
||
| if risky_paths.empty? | ||
| puts "HUMAN_SECURITY_REVIEW_NOT_REQUIRED head=#{head_sha}" | ||
| return 0 | ||
| end | ||
|
|
||
| reviews = gh_paginated_json(gh, "repos/#{repo}/pulls/#{number}/reviews") | ||
| current_reviews = reviews.select do |review| | ||
| review["commit_id"] == head_sha && | ||
| DECISIVE_REVIEW_STATES.include?(review.fetch("state", "").upcase) | ||
| end | ||
| latest_reviews = current_reviews.group_by { |review| review.dig("user", "login").to_s.downcase }.values.map do |reviewer_reviews| | ||
| reviewer_reviews.max_by do |review| | ||
| [review.fetch("submitted_at", "").to_s, review.fetch("id", 0).to_i] | ||
| end | ||
| end | ||
| approved_reviewer = latest_reviews.find do |review| | ||
| reviewer = review.dig("user", "login") | ||
| next false unless review.fetch("state", "").upcase == "APPROVED" | ||
| next false unless review.dig("user", "type") == "User" | ||
| next false if reviewer.nil? || reviewer.casecmp?(author) || reviewer.downcase.end_with?("[bot]") | ||
|
|
||
| permission = gh_json(gh, "repos/#{repo}/collaborators/#{reviewer}/permission").fetch("permission") | ||
| REVIEW_PERMISSIONS.include?(permission) | ||
| end | ||
|
|
||
| if approved_reviewer | ||
| reviewer = approved_reviewer.dig("user", "login") | ||
| puts "HUMAN_SECURITY_REVIEW_OK reviewer=@#{reviewer} head=#{head_sha}" | ||
| return 0 | ||
| end | ||
|
|
||
| warn "HUMAN_SECURITY_REVIEW_REQUIRED head=#{head_sha}" | ||
| risky_paths.each { |path| warn "- #{path}" } | ||
| 1 | ||
| rescue InfrastructureError, KeyError => e | ||
| warn "HUMAN_SECURITY_REVIEW_ERROR #{e.message}" | ||
| 70 | ||
| rescue OptionParser::ParseError => e | ||
| warn "HUMAN_SECURITY_REVIEW_ERROR #{e.message}" | ||
| 64 | ||
| end | ||
| end | ||
|
|
||
| exit HumanSecurityReviewGate.run(ARGV) if $PROGRAM_NAME == __FILE__ |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.