#!/usr/bin/env python3 """ Patch Management Workflow Automation Tracks patch compliance, generates deployment plans, and monitors patch installation success across the enterprise. Requirements: pip install requests pandas jinja2 pyyaml Usage: python process.py compliance --scan-csv scan_results.csv --asset-csv assets.csv python process.py plan --patches patches.csv --rings rings.yaml python process.py report --compliance-csv compliance.csv """ import argparse import csv import json import sys from collections import defaultdict from datetime import datetime, timedelta from pathlib import Path import pandas as pd import yaml class PatchComplianceTracker: """Track and report patch compliance across the enterprise.""" SLA_MAP = { "Critical": 2, "High": 7, "Medium": 30, "Low": 90, } def __init__(self): self.assets = pd.DataFrame() self.patches = pd.DataFrame() self.compliance = pd.DataFrame() def load_assets(self, asset_file: str): """Load asset inventory from CSV.""" self.assets = pd.read_csv(asset_file) required = {"hostname", "ip_address", "os", "tier"} if not required.issubset(set(self.assets.columns)): raise ValueError(f"Asset CSV must contain columns: {required}") print(f"[+] Loaded {len(self.assets)} assets") def load_scan_results(self, scan_file: str): """Load vulnerability scan results showing missing patches.""" self.patches = pd.read_csv(scan_file) required = {"hostname", "plugin_id", "severity", "cve", "plugin_name"} if not required.issubset(set(self.patches.columns)): raise ValueError(f"Scan CSV must contain columns: {required}") print(f"[+] Loaded {len(self.patches)} patch findings") def calculate_compliance(self) -> pd.DataFrame: """Calculate patch compliance by host and severity.""" if self.patches.empty: return pd.DataFrame() # Merge with asset data merged = self.patches.merge( self.assets[["hostname", "tier", "os"]], on="hostname", how="left" ) # Calculate per-host compliance host_compliance = [] for hostname, group in merged.groupby("hostname"): tier = group["tier"].iloc[0] if "tier" in group.columns else "Unknown" os_name = group["os"].iloc[0] if "os" in group.columns else "Unknown" critical = len(group[group["severity"] == "Critical"]) high = len(group[group["severity"] == "High"]) medium = len(group[group["severity"] == "Medium"]) low = len(group[group["severity"] == "Low"]) total = critical + high + medium + low # Compliance score: weighted by severity max_score = 100 penalty = critical * 10 + high * 5 + medium * 2 + low * 0.5 score = max(0, max_score - penalty) host_compliance.append({ "hostname": hostname, "tier": tier, "os": os_name, "critical_missing": critical, "high_missing": high, "medium_missing": medium, "low_missing": low, "total_missing": total, "compliance_score": round(score, 1), "compliant": total == 0, }) self.compliance = pd.DataFrame(host_compliance) self.compliance = self.compliance.sort_values("compliance_score") return self.compliance def get_summary(self) -> dict: """Generate compliance summary statistics.""" if self.compliance.empty: self.calculate_compliance() total_hosts = len(self.compliance) compliant = len(self.compliance[self.compliance["compliant"]]) avg_score = self.compliance["compliance_score"].mean() by_tier = {} for tier, group in self.compliance.groupby("tier"): by_tier[tier] = { "total": len(group), "compliant": len(group[group["compliant"]]), "rate": f"{len(group[group['compliant']]) / len(group) * 100:.1f}%", "avg_score": round(group["compliance_score"].mean(), 1), } return { "total_hosts": total_hosts, "compliant_hosts": compliant, "compliance_rate": f"{compliant / max(total_hosts, 1) * 100:.1f}%", "average_score": round(avg_score, 1), "total_missing_patches": int(self.compliance["total_missing"].sum()), "critical_patches_missing": int(self.compliance["critical_missing"].sum()), "by_tier": by_tier, } class DeploymentPlanner: """Generate phased patch deployment plans.""" DEFAULT_RINGS = { "ring0": {"name": "Lab/Test", "percentage": 0, "soak_hours": 48}, "ring1": {"name": "IT Early Adopters", "percentage": 5, "soak_hours": 72}, "ring2": {"name": "Business Pilot", "percentage": 15, "soak_hours": 120}, "ring3": {"name": "General Deployment", "percentage": 50, "soak_hours": 168}, "ring4": {"name": "Mission Critical", "percentage": 30, "soak_hours": 0}, } def __init__(self, rings_config: dict = None): self.rings = rings_config or self.DEFAULT_RINGS def create_deployment_plan(self, patches: list, assets: pd.DataFrame, start_date: datetime = None) -> dict: """Create a phased deployment plan for patches.""" start = start_date or datetime.now() plan = { "plan_id": f"PATCH-{start.strftime('%Y%m%d-%H%M')}", "created": start.isoformat(), "patches": patches, "rings": [], } current_date = start for ring_id, ring_config in self.rings.items(): ring_hosts = self._assign_ring_hosts( assets, ring_id, ring_config["percentage"] ) ring_plan = { "ring": ring_id, "name": ring_config["name"], "start_date": current_date.isoformat(), "end_date": (current_date + timedelta(hours=ring_config["soak_hours"])).isoformat(), "soak_hours": ring_config["soak_hours"], "host_count": len(ring_hosts), "hosts": ring_hosts, "status": "pending", "success_criteria": { "max_failure_rate": 5, "required_services_up": True, "no_critical_incidents": True, }, } plan["rings"].append(ring_plan) current_date += timedelta(hours=ring_config["soak_hours"]) plan["estimated_completion"] = current_date.isoformat() return plan def _assign_ring_hosts(self, assets: pd.DataFrame, ring_id: str, percentage: int) -> list: """Assign hosts to deployment rings based on tier and percentage.""" if assets.empty or percentage == 0: return [] ring_map = { "ring0": lambda df: df[df["tier"] == "test"], "ring1": lambda df: df[df["tier"].isin(["dev", "it"])].sample( frac=min(percentage / 100, 1.0), random_state=42 ) if len(df[df["tier"].isin(["dev", "it"])]) > 0 else pd.DataFrame(), "ring2": lambda df: df[df["tier"] == "staging"], "ring3": lambda df: df[df["tier"] == "production"].sample( frac=0.6, random_state=42 ) if len(df[df["tier"] == "production"]) > 0 else pd.DataFrame(), "ring4": lambda df: df[df["tier"].isin(["production", "critical"])], } selector = ring_map.get(ring_id) if selector: try: selected = selector(assets) return selected["hostname"].tolist() if not selected.empty else [] except (KeyError, ValueError): return [] return [] def export_plan(self, plan: dict, output_path: str): """Export deployment plan to JSON.""" with open(output_path, "w") as f: json.dump(plan, f, indent=2, default=str) print(f"[+] Deployment plan exported to: {output_path}") def generate_compliance_report(summary: dict, compliance_df: pd.DataFrame, output_path: str): """Generate HTML compliance report.""" top_noncompliant = compliance_df.head(20) html = f""" Patch Compliance Report - {datetime.now().strftime('%Y-%m-%d')}

Patch Compliance Report

Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}

{summary['compliance_rate']}

Compliance Rate

{summary['total_hosts']}

Total Hosts

{summary['total_missing_patches']}

Missing Patches

{summary['critical_patches_missing']}

Critical Missing

Compliance by Tier

{''.join(f"" for tier, data in summary['by_tier'].items())}
TierTotalCompliantRateAvg Score
{tier}{data['total']}{data['compliant']}{data['rate']}{data['avg_score']}

Top Non-Compliant Hosts

{''.join(f"" for r in top_noncompliant.itertuples())}
HostnameTierOSCriticalHighMediumScore
{r.hostname}{r.tier}{r.os}{r.critical_missing}{r.high_missing}{r.medium_missing}{r.compliance_score}
""" with open(output_path, "w", encoding="utf-8") as f: f.write(html) print(f"[+] Report saved to: {output_path}") def main(): parser = argparse.ArgumentParser(description="Patch Management Workflow Automation") subparsers = parser.add_subparsers(dest="command") comp_parser = subparsers.add_parser("compliance", help="Calculate patch compliance") comp_parser.add_argument("--scan-csv", required=True, help="Vulnerability scan results CSV") comp_parser.add_argument("--asset-csv", required=True, help="Asset inventory CSV") comp_parser.add_argument("--output", default=None, help="Output compliance CSV") comp_parser.add_argument("--report", default=None, help="Output HTML report") plan_parser = subparsers.add_parser("plan", help="Create deployment plan") plan_parser.add_argument("--patches", required=True, help="Patches CSV") plan_parser.add_argument("--assets", required=True, help="Assets CSV") plan_parser.add_argument("--rings", default=None, help="Rings config YAML") plan_parser.add_argument("--output", default="deployment_plan.json") args = parser.parse_args() if args.command == "compliance": tracker = PatchComplianceTracker() tracker.load_assets(args.asset_csv) tracker.load_scan_results(args.scan_csv) compliance = tracker.calculate_compliance() summary = tracker.get_summary() print("\n=== Patch Compliance Summary ===") print(f"Total Hosts: {summary['total_hosts']}") print(f"Compliant: {summary['compliant_hosts']}") print(f"Compliance Rate: {summary['compliance_rate']}") print(f"Missing Patches: {summary['total_missing_patches']}") print(f"Critical Missing: {summary['critical_patches_missing']}") if args.output: compliance.to_csv(args.output, index=False) print(f"[+] Compliance data saved to: {args.output}") if args.report: generate_compliance_report(summary, compliance, args.report) elif args.command == "plan": rings = None if args.rings: with open(args.rings) as f: rings = yaml.safe_load(f) planner = DeploymentPlanner(rings) assets = pd.read_csv(args.assets) patches_df = pd.read_csv(args.patches) patches = patches_df.to_dict(orient="records") plan = planner.create_deployment_plan(patches, assets) planner.export_plan(plan, args.output) print(f"\n=== Deployment Plan ===") for ring in plan["rings"]: print(f" {ring['name']}: {ring['host_count']} hosts, " f"soak: {ring['soak_hours']}h, start: {ring['start_date'][:10]}") print(f"Estimated completion: {plan['estimated_completion'][:10]}") else: parser.print_help() if __name__ == "__main__": main()