Add patch remediation feature

- --remediate: Dry run showing critical patches to approve
- --approve: Actually approve all critical patches via API
- Patches deploy during device maintenance windows

🤖 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 13:03:00 -05:00
parent f5191ffc97
commit adbd419622

View File

@@ -523,6 +523,102 @@ class NinjaOneReporter:
return html
def approve_critical_patches(self, dry_run=True):
"""Approve all critical patches across all devices"""
self.authenticate()
self.load_organizations()
self.load_devices()
print("\nLoading software patches...")
sw_result = self.api_get('/queries/software-patches')
sw_patches = sw_result.get('results', []) if isinstance(sw_result, dict) else sw_result
# Filter to critical patches that are not already approved
critical_patches = [
p for p in sw_patches
if p.get('impact', '').lower() == 'critical'
and p.get('status', '').upper() != 'APPROVED'
]
print(f"\nFound {len(critical_patches)} critical patches to approve")
if not critical_patches:
print("No critical patches need approval.")
return
# Group by title for summary
by_title = {}
for patch in critical_patches:
title = patch.get('title', 'Unknown')
if title not in by_title:
by_title[title] = []
by_title[title].append(patch)
print("\nCritical patches to approve:")
print("-" * 60)
for title, patches in sorted(by_title.items(), key=lambda x: len(x[1]), reverse=True):
device_names = [self.devices.get(p['deviceId'], {}).get('name', f"Device-{p['deviceId']}") for p in patches]
print(f" {title}: {len(patches)} devices")
for name in device_names[:5]:
print(f" - {name}")
if len(device_names) > 5:
print(f" ... and {len(device_names) - 5} more")
if dry_run:
print("\n" + "=" * 60)
print("DRY RUN - No changes made")
print("Run with --approve to actually approve these patches")
print("=" * 60)
return
# Actually approve patches
print("\n" + "=" * 60)
print("APPROVING PATCHES...")
print("=" * 60)
approved = 0
failed = 0
for patch in critical_patches:
device_id = patch.get('deviceId')
patch_id = patch.get('id')
title = patch.get('title', 'Unknown')
device_name = self.devices.get(device_id, {}).get('name', f"Device-{device_id}")
try:
# Approve the patch
headers = {'Authorization': f'Bearer {self.access_token}', 'Content-Type': 'application/json'}
url = f"{self.config['base_url']}/api/v2/device/{device_id}/software-patch/{patch_id}/approve"
response = requests.post(url, headers=headers)
if response.status_code in [200, 201, 204]:
print(f" Approved: {title} on {device_name}")
approved += 1
else:
print(f" Failed: {title} on {device_name} - {response.status_code}: {response.text[:100]}")
failed += 1
except Exception as e:
print(f" Error: {title} on {device_name} - {str(e)}")
failed += 1
print("\n" + "=" * 60)
print(f"COMPLETE: {approved} approved, {failed} failed")
print("=" * 60)
print("\nPatches will be installed during each device's maintenance window.")
print("Check NinjaOne dashboard for deployment status.")
if __name__ == '__main__':
import sys
reporter = NinjaOneReporter()
reporter.generate_report()
if '--approve' in sys.argv:
# Actually approve critical patches
reporter.approve_critical_patches(dry_run=False)
elif '--remediate' in sys.argv:
# Dry run - show what would be approved
reporter.approve_critical_patches(dry_run=True)
else:
# Default: generate report
reporter.generate_report()