ci: defer privileged recipe scans for forks (#10825)
This commit is contained in:
@@ -17,7 +17,104 @@ permissions:
|
||||
statuses: write
|
||||
|
||||
jobs:
|
||||
security-scan:
|
||||
# Forks are evaluated from GitHub file/review metadata only; recipe content
|
||||
# never enters this job or the secret-bearing scanner.
|
||||
fork-review-boundary:
|
||||
name: Fork recipe review boundary
|
||||
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
permissions:
|
||||
# GitHub App installation tokens can query collaborator permission with
|
||||
# their implicit metadata read access.
|
||||
pull-requests: read
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Require write-access approval
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const recipePrefix = 'documentation/src/pages/recipes/data/recipes/';
|
||||
const pullNumber = context.payload.pull_request.number;
|
||||
const request = {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: pullNumber,
|
||||
per_page: 100,
|
||||
};
|
||||
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, request);
|
||||
const hasReviewableRecipe = files.some(
|
||||
(file) => file.filename.startsWith(recipePrefix) && file.status !== 'removed'
|
||||
);
|
||||
|
||||
if (!hasReviewableRecipe) {
|
||||
core.notice('No added or modified recipe requires the fork review boundary.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHead = context.payload.pull_request.head.sha;
|
||||
const reviews = await github.paginate(github.rest.pulls.listReviews, request);
|
||||
const latestByReviewer = new Map();
|
||||
for (const review of reviews) {
|
||||
if (!review.user?.login || !review.submitted_at) continue;
|
||||
const state = review.state?.toUpperCase();
|
||||
if (!['APPROVED', 'CHANGES_REQUESTED', 'DISMISSED'].includes(state)) continue;
|
||||
const current = latestByReviewer.get(review.user.login);
|
||||
if (!current || new Date(review.submitted_at) > new Date(current.submitted_at)) {
|
||||
latestByReviewer.set(review.user.login, review);
|
||||
}
|
||||
}
|
||||
|
||||
const allowedPermissions = new Set(['admin', 'maintain', 'write']);
|
||||
const approvers = [];
|
||||
const blockers = [];
|
||||
for (const review of latestByReviewer.values()) {
|
||||
if (review.commit_id !== currentHead) continue;
|
||||
const state = review.state?.toUpperCase();
|
||||
if (!['APPROVED', 'CHANGES_REQUESTED'].includes(state)) continue;
|
||||
const response = await github.rest.repos.getCollaboratorPermissionLevel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
username: review.user.login,
|
||||
});
|
||||
if (allowedPermissions.has(response.data.permission)) {
|
||||
if (state === 'CHANGES_REQUESTED') {
|
||||
blockers.push(review.user.login);
|
||||
} else {
|
||||
approvers.push(review.user.login);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (blockers.length > 0) {
|
||||
core.setFailed(
|
||||
'The current fork head has changes requested by a reviewer with write access: ' +
|
||||
`@${blockers.join(', @')}.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (approvers.length === 0) {
|
||||
core.setFailed(
|
||||
'Fork recipe instructions cannot enter the secret-bearing AI scanner. ' +
|
||||
'The current head requires an approving review from a user with write access. ' +
|
||||
'After approval, rerun this failed job.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
core.notice(`Current fork head approved by @${approvers.join(', @')}.`);
|
||||
await core.summary
|
||||
.addHeading('Fork recipe review boundary')
|
||||
.addRaw(
|
||||
'The privileged AI scan was not run because this recipe comes from a fork. ' +
|
||||
`Write-access approval of the current head was verified from @${approvers.join(', @')}.`
|
||||
)
|
||||
.write();
|
||||
|
||||
origin-ai-scan:
|
||||
name: Origin recipe AI scan
|
||||
# Never load fork-controlled recipe instructions into the privileged scanner.
|
||||
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
@@ -427,3 +524,54 @@ jobs:
|
||||
echo "::error::No scan summary found - scan may have failed completely"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Preserve the existing required job context. This job always runs and
|
||||
# reflects the applicable isolated check instead of allowing a skipped
|
||||
# privileged scanner to satisfy branch protection for a fork.
|
||||
security-scan:
|
||||
name: security-scan
|
||||
if: ${{ always() }}
|
||||
needs:
|
||||
- fork-review-boundary
|
||||
- origin-ai-scan
|
||||
permissions:
|
||||
statuses: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Enforce the applicable security boundary
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||
FORK_BOUNDARY_RESULT: ${{ needs.fork-review-boundary.result }}
|
||||
ORIGIN_SCAN_RESULT: ${{ needs.origin-ai-scan.result }}
|
||||
with:
|
||||
script: |
|
||||
const isFork = process.env.IS_FORK === 'true';
|
||||
const applicableResult = isFork
|
||||
? process.env.FORK_BOUNDARY_RESULT
|
||||
: process.env.ORIGIN_SCAN_RESULT;
|
||||
const checkName = isFork ? 'fork review boundary' : 'origin recipe AI scan';
|
||||
const passed = applicableResult === 'success';
|
||||
|
||||
await github.rest.repos.createCommitStatus({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
sha: context.payload.pull_request.head.sha,
|
||||
state: passed ? 'success' : 'failure',
|
||||
target_url: `${context.payload.pull_request.html_url}/checks`,
|
||||
description: passed
|
||||
? `${checkName} passed`
|
||||
: `${checkName} result: ${applicableResult}`,
|
||||
context: 'security-scan/recipe-scanner',
|
||||
});
|
||||
|
||||
if (!passed) {
|
||||
core.setFailed(`${checkName} result: ${applicableResult}`);
|
||||
return;
|
||||
}
|
||||
|
||||
core.notice(
|
||||
isFork
|
||||
? 'Fork review boundary passed; privileged AI scan was not run.'
|
||||
: 'Origin recipe AI scan passed.'
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user