Skip to content

Commit 9223efd

Browse files
committed
feat(workflows): add closed-PR comment redirect for COE AI-7
Add closed-pr-comment.yml: detects comments on closed PRs by non-maintainers, posts a redirect to the issue tracker, and pages oncall via the existing SLACK_WEBHOOK_URL. Update slack-issue-notification.yml: switch to injection-safe env+toJSON pattern and emit a uniform 20-key payload (with event_type discriminator) so the Slack workflow can branch on event type. COE AI-7: an external user reported the SDK SPII OTEL leak by commenting on a closed revert PR; oncall does not monitor closed-PR comments, so the report was missed for ~20 hours.
1 parent 43607fa commit 9223efd

2 files changed

Lines changed: 202 additions & 8 deletions

File tree

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
name: Closed PR Comment Redirect
2+
3+
# COE AI-7: alert users who comment on closed PRs to open a GitHub issue.
4+
# An external user reported the v1.4.8 SPII leak by commenting on the
5+
# already-closed revert PR. Closed-PR comments are not surfaced to oncall,
6+
# but issues are. This workflow:
7+
# 1. Replies to the commenter with a link to open a new issue.
8+
# 2. Notifies oncall via the existing SLACK_WEBHOOK_URL.
9+
# Maintainers (admin/maintain/write) are skipped so internal back-and-forth
10+
# doesn't trigger the redirect.
11+
12+
on:
13+
issue_comment:
14+
types: [created]
15+
16+
permissions:
17+
pull-requests: write
18+
issues: read
19+
20+
jobs:
21+
redirect:
22+
runs-on: ubuntu-latest
23+
# Only fire on PRs (issue_comment fires for issues too) that are closed.
24+
if: >-
25+
github.event.issue.pull_request != null &&
26+
github.event.issue.state == 'closed' &&
27+
github.event.comment.user.type != 'Bot'
28+
steps:
29+
- name: Check commenter permission
30+
id: perm
31+
uses: actions/github-script@v9
32+
with:
33+
script: |
34+
// External users on private repos can 404 here; treat any
35+
// failure as "not a maintainer" so the redirect still fires.
36+
let permission = 'none';
37+
try {
38+
const { data } = await github.rest.repos.getCollaboratorPermissionLevel({
39+
owner: context.repo.owner,
40+
repo: context.repo.repo,
41+
username: context.payload.comment.user.login,
42+
});
43+
permission = data.permission;
44+
} catch (err) {
45+
core.info(`Permission lookup failed for ${context.payload.comment.user.login}: ${err.message}. Treating as non-maintainer.`);
46+
}
47+
const skip = ['admin', 'maintain', 'write'].includes(permission);
48+
core.setOutput('skip', String(skip));
49+
core.info(`Commenter ${context.payload.comment.user.login} permission=${permission} skip=${skip}`);
50+
51+
- name: Check for existing redirect comment
52+
id: existing
53+
if: steps.perm.outputs.skip != 'true'
54+
uses: actions/github-script@v9
55+
with:
56+
script: |
57+
// Marker we embed in our reply so we don't double-post on the same PR.
58+
const marker = '<!-- closed-pr-comment-redirect -->';
59+
const comments = await github.paginate(
60+
github.rest.issues.listComments,
61+
{
62+
owner: context.repo.owner,
63+
repo: context.repo.repo,
64+
issue_number: context.payload.issue.number,
65+
per_page: 100,
66+
}
67+
);
68+
const alreadyPosted = comments.some(c => c.body && c.body.includes(marker));
69+
core.setOutput('already_posted', String(alreadyPosted));
70+
71+
- name: Post redirect comment
72+
if: steps.perm.outputs.skip != 'true' && steps.existing.outputs.already_posted != 'true'
73+
# The Slack alert is the load-bearing part of this workflow (it's
74+
# what closes the COE detection gap). If posting the bot reply
75+
# fails (rate limit, transient error), don't block oncall paging.
76+
continue-on-error: true
77+
uses: actions/github-script@v9
78+
env:
79+
# Repos with issue templates use /issues/new/choose; repos without
80+
# templates should change this to /issues/new.
81+
ISSUES_NEW_URL: https://github.com/${{ github.repository }}/issues/new/choose
82+
with:
83+
script: |
84+
const commenter = context.payload.comment.user.login;
85+
const issuesNewUrl = process.env.ISSUES_NEW_URL;
86+
const body = [
87+
'<!-- closed-pr-comment-redirect -->',
88+
'',
89+
`Thanks for the report, @${commenter} — feedback like this is exactly`,
90+
"how we catch the things we missed. Because this PR is already",
91+
"closed, the team won't see follow-up comments here.",
92+
'',
93+
'Would you mind opening a new issue so we can track it properly?',
94+
issuesNewUrl,
95+
'',
96+
'If this is a security issue, please report it privately via',
97+
'https://aws.amazon.com/security/vulnerability-reporting/ instead',
98+
'of a public issue.',
99+
].join('\n');
100+
101+
await github.rest.issues.createComment({
102+
owner: context.repo.owner,
103+
repo: context.repo.repo,
104+
issue_number: context.payload.issue.number,
105+
body,
106+
});
107+
108+
- name: Compute PR state
109+
id: pr_state
110+
if: steps.perm.outputs.skip != 'true'
111+
uses: actions/github-script@v9
112+
with:
113+
script: |
114+
// PRs surface as `issue` events; merged_at is null when closed-not-merged.
115+
const mergedAt = context.payload.issue.pull_request &&
116+
context.payload.issue.pull_request.merged_at;
117+
core.setOutput('state', mergedAt ? 'merged' : 'closed');
118+
119+
- name: Notify Slack
120+
if: steps.perm.outputs.skip != 'true'
121+
# Attacker-controlled fields are passed through env: rather than
122+
# interpolated into the YAML payload, to prevent workflow injection.
123+
# Schema is uniform across event types: every workflow sends the
124+
# same 20 keys so Slack-side branching on event_type is reliable.
125+
# For closed-PR comments, the issue_* fields are empty (this isn't
126+
# an issue) and the pr_*/comment_* fields carry the real data.
127+
env:
128+
REPOSITORY: ${{ github.repository }}
129+
CREATED_AT: ${{ github.event.comment.created_at }}
130+
PR_NUMBER: ${{ github.event.issue.number }}
131+
PR_TITLE: ${{ github.event.issue.title }}
132+
PR_URL: ${{ github.event.issue.html_url }}
133+
PR_AUTHOR: ${{ github.event.issue.user.login }}
134+
PR_STATE: ${{ steps.pr_state.outputs.state }}
135+
PR_CLOSED_AT: ${{ github.event.issue.closed_at }}
136+
PR_MERGED_AT: ${{ github.event.issue.pull_request.merged_at }}
137+
COMMENT_ID: ${{ github.event.comment.id }}
138+
COMMENT_URL: ${{ github.event.comment.html_url }}
139+
COMMENT_AUTHOR: ${{ github.event.comment.user.login }}
140+
COMMENT_BODY: ${{ github.event.comment.body }}
141+
uses: slackapi/slack-github-action@v2.1.1
142+
with:
143+
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
144+
webhook-type: webhook-trigger
145+
payload: |
146+
event_type: "closed_pr_comment"
147+
repository: "${{ env.REPOSITORY }}"
148+
created_at: "${{ env.CREATED_AT }}"
149+
issue_number: ""
150+
issue_title: ""
151+
issue_url: ""
152+
issue_author: ""
153+
issue_body: ""
154+
labels: ""
155+
pr_number: "${{ env.PR_NUMBER }}"
156+
pr_title: ${{ toJSON(env.PR_TITLE) }}
157+
pr_url: "${{ env.PR_URL }}"
158+
pr_author: "${{ env.PR_AUTHOR }}"
159+
pr_state: "${{ env.PR_STATE }}"
160+
pr_closed_at: "${{ env.PR_CLOSED_AT }}"
161+
pr_merged_at: "${{ env.PR_MERGED_AT }}"
162+
comment_id: "${{ env.COMMENT_ID }}"
163+
comment_url: "${{ env.COMMENT_URL }}"
164+
comment_author: "${{ env.COMMENT_AUTHOR }}"
165+
comment_body: ${{ toJSON(env.COMMENT_BODY) }}

.github/workflows/slack-issue-notification.yml

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,50 @@ on:
44
issues:
55
types: [opened]
66

7+
permissions: {}
8+
79
jobs:
810
notify-slack:
911
runs-on: ubuntu-latest
1012
steps:
1113
- name: Send issue details to Slack
14+
# Attacker-controlled fields are passed through env: rather than
15+
# interpolated into the YAML payload, to prevent workflow injection.
16+
# Schema is uniform across event types: every workflow sends the
17+
# same 20 keys so Slack-side branching on event_type is reliable.
18+
# For issue_opened, the issue_* fields carry the data and the
19+
# pr_*/comment_* fields are empty.
20+
env:
21+
REPOSITORY: ${{ github.repository }}
22+
CREATED_AT: ${{ github.event.issue.created_at }}
23+
ISSUE_NUMBER: ${{ github.event.issue.number }}
24+
ISSUE_TITLE: ${{ github.event.issue.title }}
25+
ISSUE_URL: ${{ github.event.issue.html_url }}
26+
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
27+
ISSUE_BODY: ${{ github.event.issue.body }}
28+
LABELS: ${{ join(github.event.issue.labels.*.name, ', ') }}
1229
uses: slackapi/slack-github-action@v2.1.1
1330
with:
1431
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
1532
webhook-type: webhook-trigger
1633
payload: |
17-
issue_title: "${{ github.event.issue.title }}"
18-
issue_number: "${{ github.event.issue.number }}"
19-
issue_url: "${{ github.event.issue.html_url }}"
20-
issue_author: "${{ github.event.issue.user.login }}"
21-
issue_body: ${{ toJSON(github.event.issue.body) }}
22-
repository: "${{ github.repository }}"
23-
labels: "${{ join(github.event.issue.labels.*.name, ', ') }}"
24-
created_at: "${{ github.event.issue.created_at }}"
34+
event_type: "issue_opened"
35+
repository: "${{ env.REPOSITORY }}"
36+
created_at: "${{ env.CREATED_AT }}"
37+
issue_number: "${{ env.ISSUE_NUMBER }}"
38+
issue_title: ${{ toJSON(env.ISSUE_TITLE) }}
39+
issue_url: "${{ env.ISSUE_URL }}"
40+
issue_author: "${{ env.ISSUE_AUTHOR }}"
41+
issue_body: ${{ toJSON(env.ISSUE_BODY) }}
42+
labels: ${{ toJSON(env.LABELS) }}
43+
pr_number: ""
44+
pr_title: ""
45+
pr_url: ""
46+
pr_author: ""
47+
pr_state: ""
48+
pr_closed_at: ""
49+
pr_merged_at: ""
50+
comment_id: ""
51+
comment_url: ""
52+
comment_author: ""
53+
comment_body: ""

0 commit comments

Comments
 (0)