import os from dotenv import load_dotenv import logging import json import msal import requests from datetime import datetime, timezone # Load environment variables load_dotenv() # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') def get_app_registrations(): logging.info("Authenticating to Microsoft Graph API") # Azure AD app credentials from environment variables client_id = os.getenv('AZURE_CLIENT_ID') client_secret = os.getenv('AZURE_CLIENT_SECRET') tenant_id = os.getenv('AZURE_TENANT_ID') authority = f"https://login.microsoftonline.com/{tenant_id}" scope = ["https://graph.microsoft.com/.default"] # Create a confidential client application app = msal.ConfidentialClientApplication( client_id, authority=authority, client_credential=client_secret ) # Acquire a token result = app.acquire_token_for_client(scopes=scope) if "access_token" in result: logging.info("Successfully authenticated to Microsoft Graph API") access_token = result["access_token"] else: logging.error("Failed to authenticate to Microsoft Graph API") logging.error(result.get("error")) logging.error(result.get("error_description")) logging.error(result.get("correlation_id")) return [] # Fetch app registrations with owners # Updated URL to include both app registration and owner data graph_url = ( "https://graph.microsoft.com/v1.0/applications" "?$select=id,appId,displayName,passwordCredentials" "&$expand=owners($select=userPrincipalName)" ) headers = { "Authorization": f"Bearer {access_token}", "ConsistencyLevel": "eventual" } try: response = requests.get(graph_url, headers=headers) response.raise_for_status() # Debug log the raw response #logging.info(f"API Response Status: {response.status_code}") #logging.debug(f"API Response: {response.text[:1000]}...") # First 1000 chars app_registrations = response.json().get('value', []) logging.info(f"Fetched {len(app_registrations)} app registrations") return app_registrations except requests.exceptions.RequestException as e: logging.error(f"Error fetching app registrations: {e}") return [] def sort_app_registrations(app_registrations): current_date = datetime.now(timezone.utc) for app in app_registrations: if app["passwordCredentials"]: expiry_date_str = app["passwordCredentials"][0]["endDateTime"] try: expiry_date = datetime.strptime(expiry_date_str, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=timezone.utc) except ValueError: expiry_date = datetime.strptime(expiry_date_str, '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc) days_to_expiry = (expiry_date - current_date).days app["days_to_expiry"] = days_to_expiry app["expiry_date"] = expiry_date.isoformat() else: app["days_to_expiry"] = None app["expiry_date"] = None sorted_apps = sorted(app_registrations, key=lambda x: (x["days_to_expiry"] is None, x["days_to_expiry"]), reverse=False) return sorted_apps def generate_html(app_registrations): current_time = datetime.now(timezone.utc).strftime('%Y-%m-%d %H:%M:%S') html = f"""
This is an automated notification regarding expiring Azure App Registrations that you own or manage.
Why am I receiving this?
You are receiving this email because you are listed as an owner of one or more Azure App Registrations that are approaching their expiration date or have already expired.
Required Actions:
Color Coding:
If you need assistance, please contact the IT Support team.
Exported on: {current_time}
| Display Name | Expiry Date | Days to Expiry | Owners |
|---|---|---|---|
| {app['displayName']} | {expiry_date.strftime('%Y-%m-%d')} | {days_to_expiry} | {owner_list} |