From 9ce34bf4591ddc2d8e33c0812bce331a330608c0 Mon Sep 17 00:00:00 2001
From: Amudha Palani <53053396+AmudaPalani@users.noreply.github.com>
Date: Fri, 5 Sep 2025 16:36:21 -0500
Subject: [PATCH] Add device health report visualization script
---
device_reports/device_health_report.py | 250 +++++++++++++++++++++++++
1 file changed, 250 insertions(+)
create mode 100644 device_reports/device_health_report.py
diff --git a/device_reports/device_health_report.py b/device_reports/device_health_report.py
new file mode 100644
index 00000000..b687bf04
--- /dev/null
+++ b/device_reports/device_health_report.py
@@ -0,0 +1,250 @@
+#!/usr/bin/env python3
+
+"""
+Device Health Report
+This script visualizes device health status, sensor status, communication,
+data collection, and cloud connectivity from Microsoft Defender for Endpoint.
+"""
+
+import os
+import requests
+import pandas as pd
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from getpass import getpass
+
+def get_credentials():
+ """Prompt for credentials if not already set in environment."""
+ tenant_id = os.environ.get('MDE_TENANT_ID')
+ client_id = os.environ.get('MDE_CLIENT_ID')
+ client_secret = os.environ.get('MDE_CLIENT_SECRET')
+
+ if not tenant_id:
+ tenant_id = input("Enter your Tenant ID: ").strip()
+ os.environ['MDE_TENANT_ID'] = tenant_id
+
+ if not client_id:
+ client_id = input("Enter your Client ID: ").strip()
+ os.environ['MDE_CLIENT_ID'] = client_id
+
+ if not client_secret:
+ client_secret = getpass("Enter your Client Secret: ").strip()
+ os.environ['MDE_CLIENT_SECRET'] = client_secret
+
+ return tenant_id, client_id, client_secret
+
+def get_token(tenant_id, client_id, client_secret):
+ """Get OAuth2 token with WindowsDefenderATP scope."""
+ url = f'https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token'
+ data = {
+ 'client_id': client_id,
+ 'client_secret': client_secret,
+ 'scope': 'https://api.securitycenter.windows.com/.default',
+ 'grant_type': 'client_credentials'
+ }
+ print("Requesting access token...")
+ resp = requests.post(url, data=data)
+ if not resp.ok:
+ print(f"Token request failed with status {resp.status_code}")
+ print(f"Error details: {resp.text}")
+ resp.raise_for_status()
+ return resp.json()['access_token']
+
+def fetch_device_data(token):
+ """Fetch device data from Microsoft Defender API."""
+ headers = {
+ 'Authorization': f'Bearer {token}',
+ 'Content-Type': 'application/json'
+ }
+ url = 'https://api.securitycenter.windows.com/api/machines'
+ print(f"Requesting data from: {url}")
+ response = requests.get(url, headers=headers)
+ print(f"API request status: {response.status_code}")
+
+ if not response.ok:
+ print(f"API request failed: {response.text}")
+ response.raise_for_status()
+
+ return response.json()['value']
+
+def process_data(data):
+ """Process and transform the device data."""
+ df = pd.json_normalize(data)
+
+ # Map the columns from Defender API to dashboard format
+ df['Health Status'] = df['healthStatus']
+ df['Sensor'] = df['onboardingStatus'].map({'Onboarded': 'Enabled', 'NotOnboarded': 'Disabled'})
+ df['Sensor Communication'] = df['healthStatus']
+ df['Sensor Data Collection'] = df['healthStatus']
+ df['Cloud Connectivity'] = df['healthStatus']
+
+ return df
+
+def calculate_statistics(df):
+ """Calculate summary statistics from the device data."""
+ total_devices = len(df)
+ healthy_devices = df[df['Health Status'] == 'Healthy']
+ unhealthy_devices = df[df['Health Status'] != 'Healthy']
+ healthy_pct = 100 * len(healthy_devices) / total_devices if total_devices else 0
+ unhealthy_pct = 100 * len(unhealthy_devices) / total_devices if total_devices else 0
+
+ return {
+ 'total': total_devices,
+ 'healthy': len(healthy_devices),
+ 'unhealthy': len(unhealthy_devices),
+ 'healthy_pct': healthy_pct,
+ 'unhealthy_pct': unhealthy_pct
+ }
+
+def create_dashboard(df, stats):
+ """Create and display the dashboard visualizations."""
+ # Print summary statistics
+ print("\nDevice Health Summary:")
+ summary_df = pd.DataFrame({
+ 'Healthy Devices': [f'{stats["healthy_pct"]:.2f}% ({stats["healthy"]} Devices)'],
+ 'UnHealthy Devices': [f'{stats["unhealthy_pct"]:.2f}% ({stats["unhealthy"]} Devices)']
+ })
+ print(summary_df.to_string(index=False))
+
+ # Create main figure with subplots
+ fig = make_subplots(
+ rows=2, cols=1,
+ specs=[[{'type': 'domain'}],
+ [{'type': 'table'}]],
+ row_heights=[0.6, 0.4],
+ vertical_spacing=0.05
+ )
+
+ # Define metrics
+ metrics = [
+ ('Health Status', 'Healthy'),
+ ('Sensor', 'Enabled'),
+ ('Sensor Communication', 'Healthy'),
+ ('Sensor Data Collection', 'Healthy'),
+ ('Cloud Connectivity', 'Healthy')
+ ]
+
+ # Add donut charts directly to the main figure
+ for i, (col, good_val) in enumerate(metrics):
+ good = (df[col] == good_val).sum()
+ bad = stats['total'] - good
+
+ # Calculate domain positions for evenly spaced donuts
+ x_start = 0.02 + (i * 0.195) # Start at 2% and space evenly
+ x_end = x_start + 0.175 # Leave small gap between donuts
+
+ fig.add_trace(
+ go.Pie(
+ values=[good, bad],
+ labels=['Healthy', 'Unhealthy'],
+ hole=0.7,
+ name=col,
+ title=dict(
+ text=f"{col}
{good}/{stats['total']}
Healthy",
+ font=dict(size=12),
+ position="top center"
+ ),
+ marker_colors=['#21c521', '#d62728'],
+ textinfo='percent+value',
+ hoverinfo='label+percent+value',
+ domain={
+ 'x': [x_start, x_end],
+ 'y': [0.5, 0.95] # Position in upper half of first row
+ },
+ hovertemplate="%{label}
" +
+ "Count: %{value}
" +
+ "Percentage: %{percent}
" +
+ ""
+ )
+ )
+
+
+ # Create table with important columns
+ table_columns = [
+ 'computerDnsName',
+ 'osPlatform',
+ 'version',
+ 'healthStatus',
+ 'onboardingStatus',
+ 'lastIpAddress',
+ 'lastExternalIpAddress',
+ 'lastSeen'
+ ]
+
+ # Add table to the main figure
+ fig.add_trace(
+ go.Table(
+ header=dict(
+ values=[col.replace('_', ' ').title() for col in table_columns],
+ fill_color='paleturquoise',
+ align='left',
+ font=dict(size=12)
+ ),
+ cells=dict(
+ values=[df[col] for col in table_columns],
+ fill_color='lavender',
+ align='left',
+ font=dict(size=11)
+ )),
+ row=2, col=1
+ )
+
+ # Update main figure layout
+ fig.update_layout(
+ height=1200,
+ width=1500,
+ title=dict(
+ text='Device Health Dashboard',
+ x=0.5,
+ y=0.98,
+ xanchor='center',
+ yanchor='top',
+ font=dict(size=24)
+ ),
+ showlegend=False,
+ margin=dict(t=120, b=20, l=20, r=20), # Increased top margin for title
+ grid=dict(rows=2, columns=1, pattern='independent'), # Ensure independent sizing
+ annotations=[
+ # Add Device Health Overview subtitle
+ dict(
+ text="Device Health Overview",
+ xref="paper",
+ yref="paper",
+ x=0.5,
+ y=0.9, # Position above the donut charts
+ showarrow=False,
+ font=dict(size=18),
+ xanchor="center"
+ )
+ ]
+ )
+
+ # Save the combined dashboard to HTML
+ dashboard_html = 'device_health_dashboard.html'
+ fig.write_html(dashboard_html)
+ print(f"\nDashboard saved to: {dashboard_html}")
+
+ # Open the HTML file in the default browser
+ import webbrowser
+ webbrowser.open(dashboard_html)
+
+def main():
+ """Main execution function."""
+ try:
+ print("Please enter your Microsoft Defender for Endpoint API credentials:")
+ tenant_id, client_id, client_secret = get_credentials()
+
+ if not all([client_id, client_secret, tenant_id]):
+ raise ValueError("All credentials (Tenant ID, Client ID, and Client Secret) are required.")
+
+ token = get_token(tenant_id, client_id, client_secret)
+ data = fetch_device_data(token)
+ df = process_data(data)
+ stats = calculate_statistics(df)
+ create_dashboard(df, stats)
+
+ except Exception as e:
+ print(f"Error occurred: {str(e)}")
+
+if __name__ == "__main__":
+ main()