66from msal import ConfidentialClientApplication
77from datetime import datetime , timezone , timedelta
88
9- # --- Determine mode ---
9+ # --- Environment variables (loaded once) ---
10+ TENANT_ID = os .getenv ("TENANT_ID" )
11+ CLIENT_ID = os .getenv ("CLIENT_ID" )
12+ CLIENT_SECRET = os .getenv ("CLIENT_SECRET" )
13+ FROM_EMAIL = os .getenv ("FROM_EMAIL" )
14+ TO_EMAIL = os .getenv ("TO_EMAIL" )
15+ GITHUB_WEBHOOK_SECRET = os .getenv ("GITHUB_WEBHOOK_SECRET" )
16+
17+ # --- Mode toggle (Test vs Webhook server) ---
1018TEST_MODE = os .getenv ("TEST_MODE" , "true" ).lower () == "true"
1119
12- # Import Flask only if not in TEST_MODE
1320if not TEST_MODE :
1421 from flask import Flask , request
1522 app = Flask (__name__ )
1623
24+
1725# --- Microsoft Graph Email Sending Function ---
1826def send_email_via_graph (subject , body ):
19- TENANT_ID = os .getenv ("TENANT_ID" )
20- CLIENT_ID = os .getenv ("CLIENT_ID" )
21- CLIENT_SECRET = os .getenv ("CLIENT_SECRET" )
22- FROM_EMAIL = os .getenv ("FROM_EMAIL" )
23- TO_EMAIL = os .getenv ("TO_EMAIL" )
24-
27+ """Send email using Microsoft Graph API via client credentials flow."""
2528 if not all ([TENANT_ID , CLIENT_ID , CLIENT_SECRET , FROM_EMAIL , TO_EMAIL ]):
26- print ("❌ Missing required environment variables" )
29+ print ("❌ Missing required environment variables for email sending " )
2730 return
2831
2932 try :
@@ -34,6 +37,7 @@ def send_email_via_graph(subject, body):
3437 )
3538 token = app_msal .acquire_token_for_client (scopes = ["https://graph.microsoft.com/.default" ])
3639 access_token = token .get ("access_token" )
40+
3741 if not access_token :
3842 print (f"❌ Failed to get access token: { token } " )
3943 return
@@ -60,8 +64,10 @@ def send_email_via_graph(subject, body):
6064 except Exception as e :
6165 print (f"❌ Exception occurred while sending email: { e } " )
6266
63- # --- Verify GitHub webhook signature ---
67+
68+ # --- GitHub Webhook Signature Verification ---
6469def verify_github_signature (payload_body , signature , secret ):
70+ """Verify X-Hub-Signature-256 against webhook secret."""
6571 if not secret :
6672 print ("⚠️ No webhook secret set, skipping verification" )
6773 return True
@@ -73,8 +79,10 @@ def verify_github_signature(payload_body, signature, secret):
7379 expected_signature = "sha256=" + mac .hexdigest ()
7480 return hmac .compare_digest (expected_signature , signature )
7581
82+
7683# --- Convert UTC timestamp to UTC+4 ---
7784def convert_to_utc4 (timestamp ):
85+ """Convert ISO UTC timestamp string to UTC+4 formatted datetime string."""
7886 if not timestamp :
7987 return "N/A"
8088 try :
@@ -85,8 +93,10 @@ def convert_to_utc4(timestamp):
8593 print (f"⚠️ Failed to convert timestamp { timestamp } : { e } " )
8694 return timestamp
8795
96+
8897# --- Format GitHub repository event for email ---
8998def format_repo_event (repo , action ):
99+ """Generate subject + body text for repository created/deleted events."""
90100 repo_name = repo ["full_name" ]
91101 visibility_icon = "🔒 Private" if repo .get ("private" ) else "🌐 Public"
92102 owner = repo ["owner" ]["login" ]
@@ -108,20 +118,20 @@ def format_repo_event(repo, action):
108118 )
109119 return subject , body
110120
111- # --- GitHub Webhook + Health Handlers ---
121+
122+ # --- Flask Handlers (only if not in TEST_MODE) ---
112123if not TEST_MODE :
113124 @app .route ("/webhook" , methods = ["POST" ])
114125 def github_webhook ():
115126 payload_body = request .data
116127 signature = request .headers .get ("X-Hub-Signature-256" )
117- secret = os .getenv ("GITHUB_WEBHOOK_SECRET" )
118128
119129 print ("📥 Incoming GitHub webhook" )
120130 print (f"📥 Event: { request .headers .get ('X-GitHub-Event' )} " )
121131 print (f"📥 Signature header: { signature } " )
122132
123- if not verify_github_signature (payload_body , signature , secret ):
124- print (f "❌ Invalid signature! Webhook rejected." )
133+ if not verify_github_signature (payload_body , signature , GITHUB_WEBHOOK_SECRET ):
134+ print ("❌ Invalid signature! Webhook rejected." )
125135 return "❌ Invalid signature" , 401
126136
127137 try :
@@ -131,24 +141,26 @@ def github_webhook():
131141 return "❌ Bad payload" , 400
132142
133143 event = request .headers .get ("X-GitHub-Event" , "" )
144+ action = data .get ("action" , "" )
134145
135- if event == "repository" and data . get ( " action" ) in ["created" , "deleted" ]:
136- subject , body = format_repo_event (data ["repository" ], data [ " action" ] )
146+ if event == "repository" and action in ["created" , "deleted" ]:
147+ subject , body = format_repo_event (data ["repository" ], action )
137148 print (f"📩 Sending email alert: { subject } " )
138149 send_email_via_graph (subject , body )
139150 else :
140- print (f"ℹ️ Ignored event: { event } , action: { data . get ( ' action' ) } " )
151+ print (f"ℹ️ Ignored event: { event } , action: { action } " )
141152
142153 return "OK" , 200
143154
144155 @app .route ("/health" , methods = ["GET" ])
145156 def health_check ():
146157 return {"status" : "running" }, 200
147158
159+
148160# --- Main Entry Point ---
149161if __name__ == "__main__" :
150162 if TEST_MODE :
151- print ("🔹 TEST_MODE: sending test email" )
163+ print ("🔹 TEST_MODE enabled : sending test email... " )
152164 test_repo = {
153165 "full_name" : "quantori/sadsrepo" ,
154166 "private" : True ,
0 commit comments