From 4a19532b9e219b280ebff0578c986f2699f5f6ec Mon Sep 17 00:00:00 2001 From: cproudlock Date: Thu, 18 Dec 2025 12:57:10 -0500 Subject: [PATCH] Add HTML report generation with visual charts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Professional styled HTML output with summary cards - Bar charts for patch categories - Tables for org breakdown and critical items - Device attention list with critical badges - Generates TXT, CSV, and HTML simultaneously 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- report.py | 528 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 528 insertions(+) create mode 100644 report.py diff --git a/report.py b/report.py new file mode 100644 index 0000000..498370e --- /dev/null +++ b/report.py @@ -0,0 +1,528 @@ +#!/usr/bin/env python3 +""" +NinjaOne CVE Comprehensive Report +Generates detailed breakdown by category, organization, and device. +""" + +import requests +from collections import defaultdict +from datetime import datetime +import csv +import json + +try: + from config import NINJAONE_CONFIG +except ImportError: + print("Error: Copy config.example.py to config.py and add your credentials") + exit(1) + + +class NinjaOneReporter: + """Generate comprehensive CVE reports from NinjaOne""" + + def __init__(self): + self.config = NINJAONE_CONFIG + self.access_token = None + self.organizations = {} + self.devices = {} + + def authenticate(self): + """Get OAuth token""" + response = requests.post( + self.config['token_url'], + headers={'Content-Type': 'application/x-www-form-urlencoded'}, + data={ + 'grant_type': 'client_credentials', + 'client_id': self.config['client_id'], + 'client_secret': self.config['client_secret'], + 'scope': self.config['scope'] + } + ) + response.raise_for_status() + self.access_token = response.json()['access_token'] + print("Authenticated successfully") + + def api_get(self, endpoint, params=None): + """Make API GET request""" + headers = {'Authorization': f'Bearer {self.access_token}'} + url = f"{self.config['base_url']}/api/v2{endpoint}" + response = requests.get(url, headers=headers, params=params) + response.raise_for_status() + return response.json() + + def load_organizations(self): + """Load all organizations into lookup dict""" + print("Loading organizations...") + orgs = self.api_get('/organizations') + for org in orgs: + self.organizations[org['id']] = org['name'] + print(f" Loaded {len(self.organizations)} organizations") + + def load_devices(self): + """Load all devices into lookup dict""" + print("Loading devices...") + devices = self.api_get('/devices-detailed') + for device in devices: + self.devices[device['id']] = { + 'name': device.get('systemName') or device.get('dnsName') or f"Device-{device['id']}", + 'org_id': device.get('organizationId'), + 'org_name': self.organizations.get(device.get('organizationId'), 'Unknown'), + 'os': device.get('os', {}).get('name', 'Unknown') if isinstance(device.get('os'), dict) else 'Unknown' + } + print(f" Loaded {len(self.devices)} devices") + + def get_all_patches(self): + """Get all OS and software patches""" + print("Loading OS patches...") + os_result = self.api_get('/queries/os-patches') + os_patches = os_result.get('results', []) if isinstance(os_result, dict) else os_result + + print("Loading software patches...") + sw_result = self.api_get('/queries/software-patches') + sw_patches = sw_result.get('results', []) if isinstance(sw_result, dict) else sw_result + + print(f" Found {len(os_patches)} OS patches, {len(sw_patches)} software patches") + return os_patches, sw_patches + + def categorize_patch(self, patch, patch_type): + """Categorize a patch by its source/type""" + title = patch.get('title', '').lower() + + if patch_type == 'os': + return 'Windows/OS Updates' + + # Application categories + if any(x in title for x in ['chrome', 'firefox', 'edge', 'opera', 'safari']): + return 'Web Browsers' + elif any(x in title for x in ['adobe', 'acrobat', 'reader', 'flash']): + return 'Adobe Products' + elif any(x in title for x in ['java', 'jre', 'jdk', 'corretto']): + return 'Java/Runtime' + elif any(x in title for x in ['office', '365', 'word', 'excel', 'outlook']): + return 'Microsoft Office' + elif any(x in title for x in ['zoom', 'teams', 'webex', 'slack', 'gotomeeting']): + return 'Communication Apps' + elif any(x in title for x in ['vnc', 'tightvnc', 'ultravnc', 'teamviewer', 'anydesk']): + return 'Remote Access Tools' + elif any(x in title for x in ['7-zip', 'winrar', 'winzip']): + return 'Compression Tools' + elif any(x in title for x in ['vlc', 'media', 'player']): + return 'Media Players' + elif any(x in title for x in ['notepad', 'sublime', 'vscode', 'visual studio code']): + return 'Text Editors/IDEs' + elif any(x in title for x in ['putty', 'filezilla', 'winscp']): + return 'Network/Transfer Tools' + elif any(x in title for x in ['keepass', 'password']): + return 'Password Managers' + elif any(x in title for x in ['foxit', 'pdf']): + return 'PDF Tools' + elif any(x in title for x in ['libre', 'open office', 'openoffice']): + return 'Office Alternatives' + else: + return 'Other Applications' + + def generate_report(self): + """Generate comprehensive report""" + self.authenticate() + self.load_organizations() + self.load_devices() + os_patches, sw_patches = self.get_all_patches() + + # Data structures for analysis + by_category = defaultdict(lambda: {'total': 0, 'critical': 0, 'devices': set()}) + by_org = defaultdict(lambda: {'total': 0, 'critical': 0, 'os': 0, 'software': 0, 'devices': set()}) + critical_items = [] + device_details = [] + + # Process OS patches + for patch in os_patches: + device_id = patch.get('deviceId') + device = self.devices.get(device_id, {}) + org_name = device.get('org_name', 'Unknown') + impact = patch.get('impact', 'unknown').lower() + + category = 'Windows/OS Updates' + by_category[category]['total'] += 1 + by_category[category]['devices'].add(device_id) + + by_org[org_name]['total'] += 1 + by_org[org_name]['os'] += 1 + by_org[org_name]['devices'].add(device_id) + + if impact == 'critical': + by_category[category]['critical'] += 1 + by_org[org_name]['critical'] += 1 + critical_items.append({ + 'type': 'OS', + 'title': patch.get('title', 'Unknown'), + 'device_id': device_id, + 'device_name': device.get('name', 'Unknown'), + 'org': org_name, + 'status': patch.get('status', 'Unknown') + }) + + device_details.append({ + 'type': 'OS', + 'category': category, + 'title': patch.get('title', 'Unknown'), + 'impact': impact, + 'status': patch.get('status', 'Unknown'), + 'device_id': device_id, + 'device_name': device.get('name', 'Unknown'), + 'organization': org_name + }) + + # Process software patches + for patch in sw_patches: + device_id = patch.get('deviceId') + device = self.devices.get(device_id, {}) + org_name = device.get('org_name', 'Unknown') + impact = patch.get('impact', 'unknown').lower() + + category = self.categorize_patch(patch, 'software') + by_category[category]['total'] += 1 + by_category[category]['devices'].add(device_id) + + by_org[org_name]['total'] += 1 + by_org[org_name]['software'] += 1 + by_org[org_name]['devices'].add(device_id) + + if impact == 'critical': + by_category[category]['critical'] += 1 + by_org[org_name]['critical'] += 1 + critical_items.append({ + 'type': 'Software', + 'title': patch.get('title', 'Unknown'), + 'device_id': device_id, + 'device_name': device.get('name', 'Unknown'), + 'org': org_name, + 'status': patch.get('status', 'Unknown') + }) + + device_details.append({ + 'type': 'Software', + 'category': category, + 'title': patch.get('title', 'Unknown'), + 'impact': impact, + 'status': patch.get('status', 'Unknown'), + 'device_id': device_id, + 'device_name': device.get('name', 'Unknown'), + 'organization': org_name + }) + + # Generate text report + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + report = [] + report.append("=" * 70) + report.append("NINJAONE CVE COMPREHENSIVE REPORT") + report.append(f"Generated: {timestamp}") + report.append("=" * 70) + + report.append(f"\nTOTAL ORGANIZATIONS: {len(self.organizations)}") + report.append(f"TOTAL DEVICES: {len(self.devices)}") + report.append(f"TOTAL OS PATCHES: {len(os_patches)}") + report.append(f"TOTAL SOFTWARE PATCHES: {len(sw_patches)}") + report.append(f"TOTAL CRITICAL ITEMS: {len(critical_items)}") + + # Section 1: By Category + report.append("\n" + "=" * 70) + report.append("SECTION 1: PATCHES BY CATEGORY (Most Common CVE Causes)") + report.append("=" * 70) + + sorted_categories = sorted(by_category.items(), key=lambda x: x[1]['total'], reverse=True) + report.append(f"\n{'Category':<30} {'Total':>8} {'Critical':>10} {'Devices':>10}") + report.append("-" * 60) + for category, data in sorted_categories: + report.append(f"{category:<30} {data['total']:>8} {data['critical']:>10} {len(data['devices']):>10}") + + # Section 2: By Organization + report.append("\n" + "=" * 70) + report.append("SECTION 2: PATCHES BY ORGANIZATION") + report.append("=" * 70) + + sorted_orgs = sorted(by_org.items(), key=lambda x: x[1]['total'], reverse=True) + report.append(f"\n{'Organization':<35} {'Total':>7} {'Crit':>6} {'OS':>6} {'SW':>6} {'Devices':>8}") + report.append("-" * 70) + for org, data in sorted_orgs: + org_display = org[:33] + '..' if len(org) > 35 else org + report.append(f"{org_display:<35} {data['total']:>7} {data['critical']:>6} {data['os']:>6} {data['software']:>6} {len(data['devices']):>8}") + + # Section 3: Critical Items with Device Details + report.append("\n" + "=" * 70) + report.append("SECTION 3: CRITICAL ITEMS (Requires Immediate Attention)") + report.append("=" * 70) + + if critical_items: + # Group by title + critical_by_title = defaultdict(list) + for item in critical_items: + critical_by_title[item['title']].append(item) + + for title, items in sorted(critical_by_title.items(), key=lambda x: len(x[1]), reverse=True): + report.append(f"\n {title} ({len(items)} devices)") + for item in items[:10]: # Show first 10 devices per title + report.append(f" - {item['device_name']} ({item['org']}) - {item['status']}") + if len(items) > 10: + report.append(f" ... and {len(items) - 10} more devices") + else: + report.append("\n No critical items found") + + # Section 4: Devices needing attention (most patches) + report.append("\n" + "=" * 70) + report.append("SECTION 4: DEVICES NEEDING MOST ATTENTION") + report.append("=" * 70) + + device_patch_count = defaultdict(lambda: {'total': 0, 'critical': 0, 'name': '', 'org': ''}) + for detail in device_details: + did = detail['device_id'] + device_patch_count[did]['total'] += 1 + device_patch_count[did]['name'] = detail['device_name'] + device_patch_count[did]['org'] = detail['organization'] + if detail['impact'] == 'critical': + device_patch_count[did]['critical'] += 1 + + sorted_devices = sorted(device_patch_count.items(), key=lambda x: (x[1]['critical'], x[1]['total']), reverse=True) + report.append(f"\n{'Device Name':<30} {'Organization':<25} {'Total':>7} {'Critical':>10}") + report.append("-" * 75) + for device_id, data in sorted_devices[:30]: # Top 30 devices + name = data['name'][:28] + '..' if len(data['name']) > 30 else data['name'] + org = data['org'][:23] + '..' if len(data['org']) > 25 else data['org'] + report.append(f"{name:<30} {org:<25} {data['total']:>7} {data['critical']:>10}") + + report_text = "\n".join(report) + + # Save text report + report_file = f"cve_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt" + with open(report_file, 'w') as f: + f.write(report_text) + print(f"\nText report saved to: {report_file}") + + # Save CSV for detailed analysis + csv_file = f"cve_details_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + with open(csv_file, 'w', newline='') as f: + writer = csv.DictWriter(f, fieldnames=['type', 'category', 'title', 'impact', 'status', 'device_id', 'device_name', 'organization']) + writer.writeheader() + writer.writerows(device_details) + print(f"CSV details saved to: {csv_file}") + + # Generate HTML report + html_file = f"cve_report_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html" + html_content = self.generate_html_report( + timestamp, sorted_categories, sorted_orgs, critical_by_title, + sorted_devices, os_patches, sw_patches, critical_items + ) + with open(html_file, 'w') as f: + f.write(html_content) + print(f"HTML report saved to: {html_file}") + + print(report_text) + return report_text + + def generate_html_report(self, timestamp, sorted_categories, sorted_orgs, critical_by_title, sorted_devices, os_patches, sw_patches, critical_items): + """Generate HTML report""" + html = f''' + + + + + CVE Report - {timestamp} + + + +
+

🛡️ NinjaOne CVE Report

+

Generated: {timestamp}

+ +
+
+

Organizations

+
{len(self.organizations)}
+
+
+

Total Devices

+
{len(self.devices)}
+
+
+

OS Patches

+
{len(os_patches)}
+
+
+

Software Patches

+
{len(sw_patches)}
+
+
+

Critical Items

+
{len(critical_items)}
+
+
+ +

📊 Patches by Category

+
+

Understanding what types of software are causing the most CVE exposure:

+''' + # Category chart + max_total = max(d['total'] for _, d in sorted_categories) if sorted_categories else 1 + for category, data in sorted_categories: + pct = (data['total'] / max_total) * 100 + has_critical = 'has-critical' if data['critical'] > 0 else '' + critical_text = f" ({data['critical']} critical)" if data['critical'] > 0 else "" + html += f''' +
+
{category}
+
+
{data['total']}{critical_text}
+
+
{len(data['devices'])} dev
+
''' + + html += ''' +
+ +

🏢 Patches by Organization

+
+ + + + + + + + + + + + +''' + for org, data in sorted_orgs[:20]: + critical_badge = f'{data["critical"]}' if data['critical'] > 0 else '0' + html += f''' + + + + + + + + ''' + + html += ''' + +
OrganizationTotalCriticalOSSoftwareDevices
{org}{data['total']}{critical_badge}{data['os']}{data['software']}{len(data['devices'])}
+
+ +

🚨 Critical Items Requiring Immediate Attention

+
+''' + if critical_by_title: + html += '' + for title, items in sorted(critical_by_title.items(), key=lambda x: len(x[1]), reverse=True): + device_list = ', '.join([f"{i['device_name']}" for i in items[:5]]) + if len(items) > 5: + device_list += f' ... +{len(items)-5} more' + orgs = list(set([i['org'] for i in items])) + html += f''' + + + + + ''' + html += '
SoftwareAffected DevicesDetails
{title}{len(items)} devices{device_list}
Orgs: {", ".join(orgs[:3])}
' + else: + html += '

✅ No critical items found!

' + + html += ''' +
+ +

💻 Devices Needing Most Attention

+
+ + + + + + + + + + +''' + for device_id, data in sorted_devices[:25]: + critical_badge = f'{data["critical"]}' if data['critical'] > 0 else '0' + html += f''' + + + + + + ''' + + html += ''' + +
DeviceOrganizationTotal PatchesCritical
{data['name']}{data['org']}{data['total']}{critical_badge}
+
+ + +
+ +''' + return html + + +if __name__ == '__main__': + reporter = NinjaOneReporter() + reporter.generate_report()