|
36 | 36 | name: "Phase Gate Enforcement" |
37 | 37 | runs-on: ubuntu-latest |
38 | 38 | steps: |
39 | | - - name: Checkout (for phase-config.yml) |
| 39 | + - name: Checkout BASE branch (not PR — prevents self-modification attack) |
40 | 40 | uses: actions/checkout@v4 |
| 41 | + with: |
| 42 | + ref: ${{ github.event.pull_request.base.sha }} |
41 | 43 |
|
42 | 44 | - name: Install js-yaml |
43 | 45 | run: npm install js-yaml@4 --no-save --silent 2>/dev/null |
@@ -165,26 +167,39 @@ jobs: |
165 | 167 | const hasBypassLabel = prLabels.includes(bypassLabel); |
166 | 168 |
|
167 | 169 | if (hasBypassLabel) { |
168 | | - const reasonPattern = escapeHatch.reason_pattern || '## Bypass Reason'; |
169 | | - const reasonIdx = (pr.body || '').indexOf(reasonPattern); |
170 | | - if (reasonIdx === -1) { |
171 | | - addCheck('Emergency bypass', false, `Label \`${bypassLabel}\` present but no "${reasonPattern}" section in PR body. Both keys required.`); |
172 | | - // Don't short-circuit — fall through to fail |
| 170 | + // KEY 1: Actor must be in allowlist |
| 171 | + const allowedActors = escapeHatch.allowed_actors || []; |
| 172 | + const actor = context.actor; |
| 173 | + if (allowedActors.length > 0 && !allowedActors.includes(actor)) { |
| 174 | + addCheck('Emergency bypass', false, `Actor @${actor} is not authorized to invoke bypass. Allowed: ${allowedActors.join(', ')}`); |
| 175 | + // Fall through to normal enforcement (deny) |
173 | 176 | } else { |
174 | | - const reasonText = (pr.body || '').slice(reasonIdx + reasonPattern.length).trim().split('\n')[0]; |
175 | | - if (!reasonText || reasonText.length < 10) { |
176 | | - addCheck('Emergency bypass', false, 'Bypass reason too short (min 10 chars). Explain WHY this bypass is necessary.'); |
| 177 | + // KEY 2: Reason pattern must be present (not inside HTML comments) |
| 178 | + const reasonPattern = escapeHatch.reason_pattern || '## ⚠️ Bypass Reason'; |
| 179 | + const minLength = escapeHatch.min_reason_length || 30; |
| 180 | + // Strip HTML comments before searching to prevent template placeholder match |
| 181 | + const bodyNoComments = (pr.body || '').replace(/<!--[\s\S]*?-->/g, ''); |
| 182 | + const reasonIdx = bodyNoComments.indexOf(reasonPattern); |
| 183 | + if (reasonIdx === -1) { |
| 184 | + addCheck('Emergency bypass', false, `Label \`${bypassLabel}\` present but no "${reasonPattern}" section in PR body (outside HTML comments). Both keys required.`); |
| 185 | + // Don't short-circuit — fall through to fail |
177 | 186 | } else { |
178 | | - addCheck('Emergency bypass', true, `OVERRIDE ACTIVE: "${reasonText.slice(0, 100)}"`); |
179 | | -
|
180 | | - // Short-circuit: bypass granted |
181 | | - audit.verdict = 'BYPASS'; |
182 | | - audit.bypass_reason = reasonText; |
183 | | -
|
184 | | - const auditComment = formatAuditComment(audit); |
185 | | - await postAuditComment(auditComment); |
186 | | - core.warning(`⚠️ EMERGENCY BYPASS: ${reasonText.slice(0, 100)}`); |
187 | | - return; // Exit with success |
| 187 | + const reasonText = bodyNoComments.slice(reasonIdx + reasonPattern.length).trim().split('\n')[0]; |
| 188 | + if (!reasonText || reasonText.length < minLength) { |
| 189 | + addCheck('Emergency bypass', false, `Bypass reason too short (${(reasonText || '').length}/${minLength} chars). Explain WHY this bypass is necessary.`); |
| 190 | + } else { |
| 191 | + addCheck('Emergency bypass', true, `OVERRIDE by @${actor}: "${reasonText.slice(0, 100)}"`); |
| 192 | +
|
| 193 | + // Short-circuit: bypass granted |
| 194 | + audit.verdict = 'BYPASS'; |
| 195 | + audit.bypass_reason = reasonText; |
| 196 | + audit.bypass_actor = actor; |
| 197 | +
|
| 198 | + const auditComment = formatAuditComment(audit); |
| 199 | + await postAuditComment(auditComment); |
| 200 | + core.warning(`⚠️ EMERGENCY BYPASS by @${actor}: ${reasonText.slice(0, 100)}`); |
| 201 | + return; // Exit with success |
| 202 | + } |
188 | 203 | } |
189 | 204 | } |
190 | 205 | } |
|
0 commit comments