| 
 | 1 | +import os  | 
 | 2 | +import json  | 
 | 3 | +import hmac  | 
 | 4 | +import hashlib  | 
 | 5 | +import requests  | 
 | 6 | +from msal import ConfidentialClientApplication  | 
 | 7 | + | 
 | 8 | +# --- Determine mode ---  | 
 | 9 | +TEST_MODE = os.getenv("TEST_MODE", "true").lower() == "true"  | 
 | 10 | + | 
 | 11 | +# Only import Flask if not in TEST_MODE  | 
 | 12 | +if not TEST_MODE:  | 
 | 13 | +    from flask import Flask, request, jsonify  | 
 | 14 | +    app = Flask(__name__)  | 
 | 15 | + | 
 | 16 | +# --- Microsoft Graph Email Sending Function ---  | 
 | 17 | +def send_email_via_graph(subject, body):  | 
 | 18 | +    TENANT_ID = os.getenv("TENANT_ID")  | 
 | 19 | +    CLIENT_ID = os.getenv("CLIENT_ID")  | 
 | 20 | +    CLIENT_SECRET = os.getenv("CLIENT_SECRET")  | 
 | 21 | +    FROM_EMAIL = os.getenv("FROM_EMAIL")  | 
 | 22 | +    TO_EMAIL = os.getenv("TO_EMAIL")  | 
 | 23 | + | 
 | 24 | +    if not all([TENANT_ID, CLIENT_ID, CLIENT_SECRET, FROM_EMAIL, TO_EMAIL]):  | 
 | 25 | +        print("❌ Missing required environment variables")  | 
 | 26 | +        return False  | 
 | 27 | + | 
 | 28 | +    try:  | 
 | 29 | +        app_msal = ConfidentialClientApplication(  | 
 | 30 | +            CLIENT_ID,  | 
 | 31 | +            authority=f"https://login.microsoftonline.com/{TENANT_ID}",  | 
 | 32 | +            client_credential=CLIENT_SECRET  | 
 | 33 | +        )  | 
 | 34 | +        token = app_msal.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])  | 
 | 35 | +        access_token = token.get("access_token")  | 
 | 36 | +        if not access_token:  | 
 | 37 | +            print(f"❌ Failed to get access token: {token}")  | 
 | 38 | +            return False  | 
 | 39 | + | 
 | 40 | +        email_msg = {  | 
 | 41 | +            "message": {  | 
 | 42 | +                "subject": subject,  | 
 | 43 | +                "body": {"contentType": "Text", "content": body},  | 
 | 44 | +                "toRecipients": [{"emailAddress": {"address": TO_EMAIL}}]  | 
 | 45 | +            }  | 
 | 46 | +        }  | 
 | 47 | + | 
 | 48 | +        response = requests.post(  | 
 | 49 | +            f"https://graph.microsoft.com/v1.0/users/{FROM_EMAIL}/sendMail",  | 
 | 50 | +            headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},  | 
 | 51 | +            json=email_msg  | 
 | 52 | +        )  | 
 | 53 | + | 
 | 54 | +        if response.status_code == 202:  | 
 | 55 | +            print(f"✅ Email sent to {TO_EMAIL}")  | 
 | 56 | +            return True  | 
 | 57 | +        else:  | 
 | 58 | +            print(f"❌ Failed to send email: {response.status_code} {response.text}")  | 
 | 59 | +            return False  | 
 | 60 | + | 
 | 61 | +    except Exception as e:  | 
 | 62 | +        print(f"❌ Exception occurred while sending email: {e}")  | 
 | 63 | +        return False  | 
 | 64 | + | 
 | 65 | +# --- Verify GitHub webhook signature ---  | 
 | 66 | +def verify_github_signature(payload_body, signature, secret):  | 
 | 67 | +    if not secret:  | 
 | 68 | +        print("⚠️ No webhook secret set, skipping verification")  | 
 | 69 | +        return True  | 
 | 70 | +    if not signature:  | 
 | 71 | +        print("❌ No signature provided in headers")  | 
 | 72 | +        return False  | 
 | 73 | + | 
 | 74 | +    mac = hmac.new(secret.encode(), msg=payload_body, digestmod=hashlib.sha256)  | 
 | 75 | +    expected_signature = "sha256=" + mac.hexdigest()  | 
 | 76 | +    return hmac.compare_digest(expected_signature, signature)  | 
 | 77 | + | 
 | 78 | +# --- GitHub Webhook Handler ---  | 
 | 79 | +if not TEST_MODE:  | 
 | 80 | +    @app.route("/webhook", methods=["POST"])  | 
 | 81 | +    def github_webhook():  | 
 | 82 | +        payload_body = request.data  | 
 | 83 | +        signature = request.headers.get("X-Hub-Signature-256")  | 
 | 84 | +        secret = os.getenv("GITHUB_WEBHOOK_SECRET")  # ✅ Correct usage  | 
 | 85 | + | 
 | 86 | +        # Debug logging  | 
 | 87 | +        print("📥 Incoming GitHub webhook")  | 
 | 88 | +        print(f"📥 Payload size: {len(payload_body)} bytes")  | 
 | 89 | +        print(f"📥 GitHub Event: {request.headers.get('X-GitHub-Event')}")  | 
 | 90 | +        print(f"📥 Signature header: {signature}")  | 
 | 91 | +        print(f"📥 Secret length: {len(secret) if secret else 'None'}")  | 
 | 92 | + | 
 | 93 | +        if not verify_github_signature(payload_body, signature, secret):  | 
 | 94 | +            print("❌ Invalid signature! Webhook rejected.")  | 
 | 95 | +            return "❌ Invalid signature", 401  | 
 | 96 | + | 
 | 97 | +        try:  | 
 | 98 | +            data = request.json or {}  | 
 | 99 | +        except Exception as e:  | 
 | 100 | +            print(f"❌ Failed to parse JSON payload: {e}")  | 
 | 101 | +            return "❌ Bad payload", 400  | 
 | 102 | + | 
 | 103 | +        event = request.headers.get("X-GitHub-Event", "")  | 
 | 104 | + | 
 | 105 | +        # Handle repository create/delete events  | 
 | 106 | +        if event == "repository" and data.get("action") in ["created", "deleted"]:  | 
 | 107 | +            repo_name = data["repository"]["full_name"]  | 
 | 108 | +            action = data["action"]  | 
 | 109 | + | 
 | 110 | +            subject = f"[GitHub Alert] Repository {action}: {repo_name}"  | 
 | 111 | +            body = (  | 
 | 112 | +                f"A repository was {action} in your GitHub organization.\n\n"  | 
 | 113 | +                f"Details:\n{json.dumps(data, indent=2)}"  | 
 | 114 | +            )  | 
 | 115 | + | 
 | 116 | +            print(f"📩 Sending email alert: {subject}")  | 
 | 117 | +            send_email_via_graph(subject, body)  | 
 | 118 | +        else:  | 
 | 119 | +            print(f"ℹ️ Ignored event: {event}, action: {data.get('action')}")  | 
 | 120 | + | 
 | 121 | +        return "OK", 200  | 
 | 122 | + | 
 | 123 | +    # Health check endpoint  | 
 | 124 | +    @app.route("/health", methods=["GET"])  | 
 | 125 | +    def health_check():  | 
 | 126 | +        return jsonify({"status": "running"}), 200  | 
 | 127 | + | 
 | 128 | +# --- Main Entry Point ---  | 
 | 129 | +if __name__ == "__main__":  | 
 | 130 | +    if TEST_MODE:  | 
 | 131 | +        print("🔹 TEST_MODE: sending test email")  | 
 | 132 | +        send_email_via_graph(  | 
 | 133 | +            "[Test] Graph Email",  | 
 | 134 | +            "This is a test email sent via Microsoft Graph with application permissions."  | 
 | 135 | +        )  | 
 | 136 | +    else:  | 
 | 137 | +        print("✅ Flask is up and listening on /webhook and /health")  | 
 | 138 | +        # Azure expects port 8000 by default for containerized apps  | 
 | 139 | +        port = int(os.getenv("PORT", 8000))  | 
 | 140 | +        app.run(host="0.0.0.0", port=port)  | 
0 commit comments