Skip to content

Commit 7fba467

Browse files
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.
1 parent 08d82bc commit 7fba467

File tree

1 file changed

+281
-0
lines changed

1 file changed

+281
-0
lines changed
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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+
// Set outputs for notifications
189+
core.setOutput('changes_detected', changesDetected.toString());
190+
core.setOutput('change_description', changeDescription);
191+
192+
} catch (error) {
193+
console.log('Error details:', error);
194+
core.setFailed(`Error: ${error.message}`);
195+
}
196+
197+
- name: Send Slack Notification - Changes Detected
198+
if: steps.check-rules.outputs.changes_detected == 'true'
199+
uses: slackapi/[email protected]
200+
env:
201+
SLACK_BOT_TOKEN: ${{ secrets.BRANCH_PROTECTION_SLACK_BOT_TOKEN }}
202+
with:
203+
channel-id: 'C081N38PHC5'
204+
payload: |
205+
{
206+
"text": "🚨 Branch protection rules changed in ${{ github.repository }}",
207+
"blocks": [
208+
{
209+
"type": "section",
210+
"text": {
211+
"type": "mrkdwn",
212+
"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>"
213+
}
214+
},
215+
{
216+
"type": "section",
217+
"text": {
218+
"type": "mrkdwn",
219+
"text": "Branch protection rules have changed. Check repository settings for details."
220+
}
221+
},
222+
{
223+
"type": "actions",
224+
"elements": [
225+
{
226+
"type": "button",
227+
"text": {
228+
"type": "plain_text",
229+
"text": "View Branch Settings",
230+
"emoji": true
231+
},
232+
"url": "${{ github.server_url }}/${{ github.repository }}/settings/branches"
233+
},
234+
{
235+
"type": "button",
236+
"text": {
237+
"type": "plain_text",
238+
"text": "View Workflow Run",
239+
"emoji": true
240+
},
241+
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
242+
}
243+
]
244+
}
245+
]
246+
}
247+
248+
- name: Send Slack Notification - Error
249+
if: failure()
250+
uses: slackapi/[email protected]
251+
with:
252+
channel-id: 'C081N38PHC5'
253+
payload: |
254+
{
255+
"text": "⚠️ Error monitoring branch protection in ${{ github.repository }}",
256+
"blocks": [
257+
{
258+
"type": "section",
259+
"text": {
260+
"type": "mrkdwn",
261+
"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>"
262+
}
263+
},
264+
{
265+
"type": "actions",
266+
"elements": [
267+
{
268+
"type": "button",
269+
"text": {
270+
"type": "plain_text",
271+
"text": "View Error Details"
272+
},
273+
"url": "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}",
274+
"style": "danger"
275+
}
276+
]
277+
}
278+
]
279+
}
280+
env:
281+
SLACK_BOT_TOKEN: ${{ secrets.BRANCH_PROTECTION_SLACK_BOT_TOKEN }}

0 commit comments

Comments
 (0)