#!/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'''
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''' ''' html += '''| Organization | Total | Critical | OS | Software | Devices |
|---|---|---|---|---|---|
| {org} | {data['total']} | {critical_badge} | {data['os']} | {data['software']} | {len(data['devices'])} |
| Software | Affected Devices | Details |
|---|---|---|
| {title} | {len(items)} devices | {device_list} Orgs: {", ".join(orgs[:3])} |
No critical items found.
' html += '''| Device | Organization | Total Patches | Critical |
|---|---|---|---|
| {data['name']} | {data['org']} | {data['total']} | {critical_badge} |