Add patches needing approval section to reports

- Add Section 4 showing MANUAL status patches grouped by org/device
- Fix OS patch field mapping (name/severity instead of title/impact)
- Add pending approval count to HTML summary cards
- Update .gitignore to exclude HTML and CSV report files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2025-12-18 16:34:22 -05:00
parent 3615e318b9
commit d14432ce59
2 changed files with 89 additions and 7 deletions

2
.gitignore vendored
View File

@@ -12,6 +12,8 @@ ENV/
# Reports
cve_report_*.txt
cve_report_*.html
cve_details_*.csv
*.log
# IDE

View File

@@ -135,11 +135,15 @@ class NinjaOneReporter:
device_details = []
# Process OS patches
# OS patches use 'name' and 'severity' fields
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()
# OS patches use 'severity' not 'impact'
impact = patch.get('severity', patch.get('impact', 'unknown')).lower()
# OS patches use 'name' not 'title'
title = patch.get('name', patch.get('title', 'Unknown'))
category = 'Windows/OS Updates'
by_category[category]['total'] += 1
@@ -154,7 +158,7 @@ class NinjaOneReporter:
by_org[org_name]['critical'] += 1
critical_items.append({
'type': 'OS',
'title': patch.get('title', 'Unknown'),
'title': title,
'device_id': device_id,
'device_name': device.get('name', 'Unknown'),
'org': org_name,
@@ -164,7 +168,7 @@ class NinjaOneReporter:
device_details.append({
'type': 'OS',
'category': category,
'title': patch.get('title', 'Unknown'),
'title': title,
'impact': impact,
'status': patch.get('status', 'Unknown'),
'device_id': device_id,
@@ -267,9 +271,38 @@ class NinjaOneReporter:
else:
report.append("\n No critical items found")
# Section 4: Devices needing attention (most patches)
# Section 4: Patches Needing Approval (MANUAL status)
report.append("\n" + "=" * 70)
report.append("SECTION 4: DEVICES NEEDING MOST ATTENTION")
report.append("SECTION 4: PATCHES NEEDING APPROVAL (Manual Policy Review)")
report.append("=" * 70)
manual_patches = [d for d in device_details if d['status'] == 'MANUAL']
if manual_patches:
# Group by organization, then device
manual_by_org = defaultdict(lambda: defaultdict(list))
for patch in manual_patches:
manual_by_org[patch['organization']][patch['device_name']].append(patch)
report.append(f"\n Total patches needing approval: {len(manual_patches)}")
report.append(f" Across {len(manual_by_org)} organizations\n")
for org in sorted(manual_by_org.keys()):
devices = manual_by_org[org]
total_patches = sum(len(patches) for patches in devices.values())
report.append(f"\n {org} ({total_patches} patches, {len(devices)} devices)")
report.append(" " + "-" * 50)
for device_name in sorted(devices.keys()):
patches = devices[device_name]
report.append(f" {device_name}:")
for patch in patches:
impact_marker = "[CRIT]" if patch['impact'] == 'critical' else "[" + patch['impact'][:4].upper() + "]"
report.append(f" {impact_marker} {patch['title']}")
else:
report.append("\n No patches pending manual approval")
# Section 5: Devices needing attention (most patches)
report.append("\n" + "=" * 70)
report.append("SECTION 5: DEVICES NEEDING MOST ATTENTION")
report.append("=" * 70)
device_patch_count = defaultdict(lambda: {'total': 0, 'critical': 0, 'name': '', 'org': ''})
@@ -309,7 +342,7 @@ class NinjaOneReporter:
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
sorted_devices, os_patches, sw_patches, critical_items, manual_by_org
)
with open(html_file, 'w') as f:
f.write(html_content)
@@ -318,7 +351,7 @@ class NinjaOneReporter:
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):
def generate_html_report(self, timestamp, sorted_categories, sorted_orgs, critical_by_title, sorted_devices, os_patches, sw_patches, critical_items, manual_by_org=None):
"""Generate HTML report"""
html = f'''<!DOCTYPE html>
<html lang="en">
@@ -407,6 +440,10 @@ class NinjaOneReporter:
<h3>Critical Items</h3>
<div class="number">{len(critical_items)}</div>
</div>
<div class="card warning">
<h3>Pending Approval</h3>
<div class="number">{sum(len(patches) for devices in (manual_by_org or {}).values() for patches in devices.values())}</div>
</div>
</div>
<h2>Patches by Category</h2>
@@ -486,6 +523,42 @@ class NinjaOneReporter:
html += '''
</div>
<h2>Patches Needing Approval</h2>
<div class="section">
<p>These patches require manual policy approval before they can be deployed:</p>
'''
if manual_by_org:
total_manual = sum(
len(patches)
for devices in manual_by_org.values()
for patches in devices.values()
)
html += f'<p><strong>Total:</strong> {total_manual} patches across {len(manual_by_org)} organizations</p>'
html += '<table><thead><tr><th>Organization</th><th>Device</th><th>Patches Needing Approval</th></tr></thead><tbody>'
for org in sorted(manual_by_org.keys()):
devices = manual_by_org[org]
first_org = True
for device_name in sorted(devices.keys()):
patches = devices[device_name]
patch_list = '<br>'.join([
f'<span class="badge badge-{"critical" if p["impact"]=="critical" else "medium"}">{p["impact"][:4].upper()}</span> {p["title"]}'
for p in patches
])
org_cell = f'<td rowspan="{len(devices)}"><strong>{org}</strong></td>' if first_org else ''
first_org = False
html += f'''
<tr>
{org_cell}
<td>{device_name}</td>
<td>{patch_list}</td>
</tr>'''
html += '</tbody></table>'
else:
html += '<p style="color: #276749;">No patches pending manual approval.</p>'
html += '''
</div>
<h2>Devices Needing Most Attention</h2>
<div class="section">
<table>
@@ -557,6 +630,13 @@ class NinjaOneReporter:
print(f"\nFound {len(critical_patches)} critical patches on {len(devices_to_patch)} devices")
# Show status breakdown
from collections import Counter
statuses = Counter(p.get('status') for p in critical_patches)
print("\nPatch status breakdown:")
for status, count in statuses.most_common():
print(f" {status}: {count}")
if not devices_to_patch:
print("No devices need patching.")
return