Skip to content

Commit 5a5792b

Browse files
Branch Protection Monitor Workflow (#20386)
* Add files via upload This Pull Requests adds a workflow to monitor for changes to branch protection rules in this repository and sends notifications to Slack. * Update Monitor Branch Protection Changes.yml The workflow has been updated to automatically lock branch-state issues and send notifications when change-event triggers occur. This improvement also mitigates risks associated with malicious modifications or non-functional scripts. * Rename Monitor Branch Protection Changes.yml to .github/workflows/Monitor Branch Protection Changes.yml
1 parent 490f507 commit 5a5792b

File tree

1 file changed

+349
-0
lines changed

1 file changed

+349
-0
lines changed
Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
name: Monitor Branch Protection Changes
2+
on:
3+
branch_protection_rule:
4+
types: [created, edited, deleted]
5+
schedule:
6+
- cron: '0 0 * * *' # Runs at 00:00 UTC every day
7+
workflow_dispatch:
8+
issues:
9+
types: [edited] # Trigger on issue edits
10+
11+
jobs:
12+
check-tampering:
13+
if: contains(github.event.issue.labels.*.name, 'branch-protection-state')
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Send Tampering Alert
17+
uses: slackapi/[email protected]
18+
env:
19+
SLACK_BOT_TOKEN: ${{ secrets.BRANCH_PROTECTION_SLACK_BOT_TOKEN }}
20+
with:
21+
channel-id: 'C081N38PHC5'
22+
payload: |
23+
{
24+
"text": "⚠️ SECURITY ALERT: Branch Protection State Issue Modified",
25+
"blocks": [
26+
{
27+
"type": "section",
28+
"text": {
29+
"type": "mrkdwn",
30+
"text": "🚨 *SECURITY ALERT: Branch Protection State Issue was manually modified!*\n\n*Repository:* ${{ github.repository }}\n*Modified by:* ${{ github.actor }}\n*Issue:* #${{ github.event.issue.number }}\n*Time:* ${{ github.event.issue.updated_at }}"
31+
}
32+
},
33+
{
34+
"type": "section",
35+
"text": {
36+
"type": "mrkdwn",
37+
"text": "⚠️ Manual modification of state issues may indicate an attempt to bypass branch protection monitoring. Please investigate immediately."
38+
}
39+
},
40+
{
41+
"type": "actions",
42+
"elements": [
43+
{
44+
"type": "button",
45+
"text": {
46+
"type": "plain_text",
47+
"text": "View Modified Issue",
48+
"emoji": true
49+
},
50+
"url": "${{ github.event.issue.html_url }}",
51+
"style": "danger"
52+
},
53+
{
54+
"type": "button",
55+
"text": {
56+
"type": "plain_text",
57+
"text": "View Branch Settings",
58+
"emoji": true
59+
},
60+
"url": "${{ github.server_url }}/${{ github.repository }}/settings/branches"
61+
}
62+
]
63+
}
64+
]
65+
}
66+
67+
check-branch-protection:
68+
# Don't run the normal check if this is an issue edit event
69+
if: github.event_name != 'issues'
70+
runs-on: ubuntu-latest
71+
72+
steps:
73+
- name: Check Branch Protection Rules
74+
id: check-rules
75+
uses: actions/github-script@v7
76+
with:
77+
github-token: ${{ secrets.BRANCH_PROTECTION_PAT }}
78+
script: |
79+
const repo = context.repo;
80+
81+
try {
82+
// Get repository info
83+
console.log('Getting repository info...');
84+
const repoInfo = await github.rest.repos.get({
85+
owner: repo.owner,
86+
repo: repo.repo
87+
});
88+
89+
const defaultBranch = repoInfo.data.default_branch;
90+
console.log(`Default branch is: ${defaultBranch}`);
91+
92+
// Get current protection rules
93+
console.log(`Checking protection rules for ${defaultBranch}...`);
94+
let currentRules;
95+
try {
96+
const protection = await github.rest.repos.getBranchProtection({
97+
owner: repo.owner,
98+
repo: repo.repo,
99+
branch: defaultBranch
100+
});
101+
currentRules = protection.data;
102+
console.log('Current protection rules:', JSON.stringify(currentRules, null, 2));
103+
} catch (error) {
104+
if (error.status === 404) {
105+
console.log('No branch protection rules found');
106+
currentRules = null;
107+
} else {
108+
console.log('Error getting branch protection:', error);
109+
throw error;
110+
}
111+
}
112+
113+
// Find previous state issues
114+
console.log('Finding previous state issues...');
115+
const previousIssues = await github.rest.issues.listForRepo({
116+
owner: repo.owner,
117+
repo: repo.repo,
118+
state: 'open',
119+
labels: 'branch-protection-state',
120+
per_page: 100
121+
});
122+
123+
console.log(`Found ${previousIssues.data.length} previous state issues`);
124+
125+
let previousRules = null;
126+
let changesDetected = false;
127+
let changeDescription = '';
128+
129+
// Always create a new state issue if none exists
130+
if (previousIssues.data.length === 0) {
131+
console.log('No previous state issues found, creating initial state');
132+
changesDetected = true;
133+
changeDescription = 'Initial branch protection state';
134+
} else {
135+
// Get the most recent state
136+
const mostRecentIssue = previousIssues.data[0];
137+
try {
138+
previousRules = JSON.parse(mostRecentIssue.body);
139+
console.log('Successfully parsed previous rules');
140+
141+
// Compare states
142+
const currentJSON = JSON.stringify(currentRules);
143+
const previousJSON = JSON.stringify(previousRules);
144+
145+
if (currentJSON !== previousJSON) {
146+
changesDetected = true;
147+
console.log('Changes detected!');
148+
149+
if (!previousRules && currentRules) {
150+
changeDescription = 'Branch protection rules were added';
151+
} else if (previousRules && !currentRules) {
152+
changeDescription = 'Branch protection rules were removed';
153+
} else {
154+
changeDescription = 'Branch protection rules were modified';
155+
}
156+
}
157+
158+
// Close all previous state issues
159+
console.log('Closing previous state issues...');
160+
for (const issue of previousIssues.data) {
161+
await github.rest.issues.update({
162+
owner: repo.owner,
163+
repo: repo.repo,
164+
issue_number: issue.number,
165+
state: 'closed'
166+
});
167+
console.log(`Closed issue #${issue.number}`);
168+
}
169+
} catch (e) {
170+
console.log('Error handling previous state:', e);
171+
// If we can't parse previous state, treat as initial
172+
changesDetected = true;
173+
changeDescription = 'Initial branch protection state (previous state invalid)';
174+
}
175+
}
176+
177+
// Always create a new state issue
178+
console.log('Creating new state issue...');
179+
const newIssue = await github.rest.issues.create({
180+
owner: repo.owner,
181+
repo: repo.repo,
182+
title: `Branch Protection State - ${new Date().toISOString()}`,
183+
body: JSON.stringify(currentRules, null, 2),
184+
labels: ['branch-protection-state']
185+
});
186+
console.log(`Created new state issue #${newIssue.data.number}`);
187+
188+
// Lock the issue immediately
189+
await github.rest.issues.lock({
190+
owner: repo.owner,
191+
repo: repo.repo,
192+
issue_number: newIssue.data.number,
193+
lock_reason: 'resolved'
194+
});
195+
console.log(`Locked issue #${newIssue.data.number}`);
196+
197+
// Set outputs for notifications
198+
core.setOutput('changes_detected', changesDetected.toString());
199+
core.setOutput('change_description', changeDescription);
200+
201+
} catch (error) {
202+
console.log('Error details:', error);
203+
core.setFailed(`Error: ${error.message}`);
204+
}
205+
206+
- name: Send Slack Notification - Branch Protection Event
207+
if: github.event_name == 'branch_protection_rule'
208+
uses: slackapi/[email protected]
209+
env:
210+
SLACK_BOT_TOKEN: ${{ secrets.BRANCH_PROTECTION_SLACK_BOT_TOKEN }}
211+
with:
212+
channel-id: 'C081N38PHC5'
213+
payload: |
214+
{
215+
"text": "⚠️ Branch Protection Change Event Detected in ${{ github.repository }}",
216+
"blocks": [
217+
{
218+
"type": "header",
219+
"text": {
220+
"type": "plain_text",
221+
"text": "⚠️ Branch Protection Change Event Detected",
222+
"emoji": true
223+
}
224+
},
225+
{
226+
"type": "section",
227+
"text": {
228+
"type": "mrkdwn",
229+
"text": "*Repository:* ${{ github.repository }}\n*Triggered by:* ${{ github.actor }}\n*Event Type:* ${{ github.event.action }}\n*Workflow Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
230+
}
231+
},
232+
{
233+
"type": "section",
234+
"text": {
235+
"type": "mrkdwn",
236+
"text": "⚠️ *Monitoring Notice:* A branch protection change event was triggered. If no change notification follows, this could indicate a non-working script or potential malicious activity."
237+
}
238+
},
239+
{
240+
"type": "actions",
241+
"elements": [
242+
{
243+
"type": "button",
244+
"text": {
245+
"type": "plain_text",
246+
"text": "View Branch Settings",
247+
"emoji": true
248+
},
249+
"url": "${{ github.server_url }}/${{ github.repository }}/settings/branches"
250+
},
251+
{
252+
"type": "button",
253+
"text": {
254+
"type": "plain_text",
255+
"text": "View Workflow Run",
256+
"emoji": true
257+
},
258+
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
259+
}
260+
]
261+
}
262+
]
263+
}
264+
265+
- name: Send Slack Notification - Changes Detected
266+
if: steps.check-rules.outputs.changes_detected == 'true'
267+
uses: slackapi/[email protected]
268+
env:
269+
SLACK_BOT_TOKEN: ${{ secrets.BRANCH_PROTECTION_SLACK_BOT_TOKEN }}
270+
with:
271+
channel-id: 'C081N38PHC5'
272+
payload: |
273+
{
274+
"text": "🚨 Branch protection rules changed in ${{ github.repository }}",
275+
"blocks": [
276+
{
277+
"type": "section",
278+
"text": {
279+
"type": "mrkdwn",
280+
"text": "🚨 *Branch protection rules changed!*\n\n*Repository:* ${{ github.repository }}\n*Changed by:* ${{ github.actor }}\n*Event:* ${{ github.event_name }}\n*Change:* ${{ steps.check-rules.outputs.change_description }}\n*Workflow Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
281+
}
282+
},
283+
{
284+
"type": "section",
285+
"text": {
286+
"type": "mrkdwn",
287+
"text": "Branch protection rules have changed. Check repository settings for details."
288+
}
289+
},
290+
{
291+
"type": "actions",
292+
"elements": [
293+
{
294+
"type": "button",
295+
"text": {
296+
"type": "plain_text",
297+
"text": "View Branch Settings",
298+
"emoji": true
299+
},
300+
"url": "${{ github.server_url }}/${{ github.repository }}/settings/branches"
301+
},
302+
{
303+
"type": "button",
304+
"text": {
305+
"type": "plain_text",
306+
"text": "View Workflow Run",
307+
"emoji": true
308+
},
309+
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
310+
}
311+
]
312+
}
313+
]
314+
}
315+
316+
- name: Send Slack Notification - Error
317+
if: failure()
318+
uses: slackapi/[email protected]
319+
env:
320+
SLACK_BOT_TOKEN: ${{ secrets.BRANCH_PROTECTION_SLACK_BOT_TOKEN }}
321+
with:
322+
channel-id: 'C081N38PHC5'
323+
payload: |
324+
{
325+
"text": "⚠️ Error monitoring branch protection in ${{ github.repository }}",
326+
"blocks": [
327+
{
328+
"type": "section",
329+
"text": {
330+
"type": "mrkdwn",
331+
"text": "⚠️ *Error monitoring branch protection!*\n\n*Repository:* ${{ github.repository }}\n*Workflow Run:* <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|View Details>"
332+
}
333+
},
334+
{
335+
"type": "actions",
336+
"elements": [
337+
{
338+
"type": "button",
339+
"text": {
340+
"type": "plain_text",
341+
"text": "View Error Details"
342+
},
343+
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
344+
"style": "danger"
345+
}
346+
]
347+
}
348+
]
349+
}

0 commit comments

Comments
 (0)