vulnerability management

Implementing Patch Management Workflow

Patch management is the systematic process of identifying, testing, deploying, and verifying software updates to remediate vulnerabilities across an organization's IT infrastructure. An effective patch management workflow reduces the attack surface while minimizing operational disruption through structured testing, approval gates, and phased rollouts.

ansiblepatch-managementrisksccmvulnerability-managementwsus
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

Overview

Patch management is the systematic process of identifying, testing, deploying, and verifying software updates to remediate vulnerabilities across an organization's IT infrastructure. An effective patch management workflow reduces the attack surface while minimizing operational disruption through structured testing, approval gates, and phased rollouts.

When to Use

  • When deploying or configuring implementing patch management workflow capabilities in your environment
  • When establishing security controls aligned to compliance requirements
  • When building or improving security architecture for this domain
  • When conducting security assessments that require this implementation

Prerequisites

  • Vulnerability scan results identifying missing patches
  • Patch management tools (WSUS, SCCM/MECM, Ansible, Intune, Jamf)
  • Test environment mirroring production
  • Change management process (ITIL or equivalent)
  • Asset inventory with OS and application versions

Core Concepts

Patch Lifecycle Phases

  1. Discovery: Identify available patches from vendors and vulnerability scans
  2. Assessment: Evaluate patch applicability and risk
  3. Prioritization: Rank patches by severity, exploitability, and asset criticality
  4. Testing: Validate patches in non-production environment
  5. Approval: Change advisory board (CAB) review and approval
  6. Deployment: Phased rollout to production systems
  7. Verification: Confirm successful installation and no regressions
  8. Reporting: Document compliance metrics and exceptions

Patch Categories

  • Security Patches: Address CVEs and security vulnerabilities
  • Critical Updates: Non-security bug fixes affecting stability
  • Service Packs: Cumulative update collections
  • Feature Updates: New functionality (Windows feature updates, etc.)
  • Firmware Updates: BIOS/UEFI, NIC, storage controller firmware
  • Third-Party Patches: Adobe, Java, Chrome, Firefox, etc.

Deployment Rings (Phased Rollout)

Ring Environment % of Fleet Soak Time Purpose
Ring 0 Lab/Test N/A 24-48 hrs Functional validation
Ring 1 IT Early Adopters 5% 48-72 hrs Real-world pilot
Ring 2 Business Pilot 15% 5-7 days Broader compatibility
Ring 3 General Deployment 50% 7-14 days Main rollout
Ring 4 Mission Critical 30% After Ring 3 Final deployment

Workflow

Step 1: Configure Patch Sources

# WSUS (Windows Server Update Services)
# Configure WSUS server to sync with Microsoft Update
# Via PowerShell on WSUS server:
Install-WindowsFeature -Name UpdateServices -IncludeManagementTools
& "C:\Program Files\Update Services\Tools\WsusUtil.exe" postinstall CONTENT_DIR=D:\WSUS
 
# Configure GPO for WSUS clients
# Computer Configuration > Administrative Templates > Windows Components > Windows Update
# Specify intranet Microsoft update service location: http://wsus-server:8530
# Ansible: Configure patch repositories for Linux
# roles/patch-management/tasks/configure_repos.yml
---
- name: Configure RHEL patch repository
  yum_repository:
    name: rhel-patches
    description: RHEL Security Patches
    baseurl: https://satellite.corp.local/pulp/repos/patches
    gpgcheck: yes
    gpgkey: file:///etc/pki/rpm-gpg/RPM-GPG-KEY-redhat-release
    enabled: yes
 
- name: Configure Ubuntu patch sources
  apt_repository:
    repo: "deb https://apt-mirror.corp.local/ubuntu {{ ansible_distribution_release }}-security main"
    state: present
  when: ansible_os_family == "Debian"

Step 2: Automated Patch Assessment

# patch_assessment.py - Correlate vulnerability scans with available patches
import subprocess
import platform
import json
 
def get_windows_pending_patches():
    """Query Windows Update for pending patches via PowerShell."""
    ps_cmd = """
    $Session = New-Object -ComObject Microsoft.Update.Session
    $Searcher = $Session.CreateUpdateSearcher()
    $Results = $Searcher.Search("IsInstalled=0 AND Type='Software'")
    $Results.Updates | ForEach-Object {
        [PSCustomObject]@{
            Title = $_.Title
            KB = ($_.KBArticleIDs -join ',')
            Severity = $_.MsrcSeverity
            Size = [math]::Round($_.MaxDownloadSize / 1MB, 2)
            Published = $_.LastDeploymentChangeTime.ToString('yyyy-MM-dd')
            CVE = ($_.CveIDs -join ',')
        }
    } | ConvertTo-Json
    """
    result = subprocess.run(
        ["powershell", "-Command", ps_cmd],
        capture_output=True, text=True, timeout=120
    )
    return json.loads(result.stdout) if result.stdout.strip() else []
 
def get_linux_pending_patches():
    """Query package manager for available security updates."""
    if platform.system() != "Linux":
        return []
 
    # Try apt (Debian/Ubuntu)
    try:
        result = subprocess.run(
            ["apt", "list", "--upgradable"],
            capture_output=True, text=True, timeout=60
        )
        packages = []
        for line in result.stdout.strip().split("\n")[1:]:
            if line:
                parts = line.split("/")
                packages.append({
                    "package": parts[0],
                    "available_version": parts[1].split()[0] if len(parts) > 1 else "",
                    "source": "apt"
                })
        return packages
    except FileNotFoundError:
        pass
 
    # Try yum/dnf (RHEL/CentOS)
    try:
        result = subprocess.run(
            ["dnf", "updateinfo", "list", "security", "--available"],
            capture_output=True, text=True, timeout=60
        )
        packages = []
        for line in result.stdout.strip().split("\n"):
            parts = line.split()
            if len(parts) >= 3:
                packages.append({
                    "advisory": parts[0],
                    "severity": parts[1],
                    "package": parts[2],
                    "source": "dnf"
                })
        return packages
    except FileNotFoundError:
        return []

Step 3: Patch Testing Automation

# Ansible playbook: test_patches.yml
---
- name: Test Patches in Lab Environment
  hosts: test_servers
  become: yes
  vars:
    rollback_snapshot: "pre-patch-{{ ansible_date_time.date }}"
 
  tasks:
    - name: Create VM snapshot before patching
      community.vmware.vmware_guest_snapshot:
        hostname: "{{ vcenter_host }}"
        username: "{{ vcenter_user }}"
        password: "{{ vcenter_pass }}"
        datacenter: "{{ datacenter }}"
        name: "{{ inventory_hostname }}"
        snapshot_name: "{{ rollback_snapshot }}"
        state: present
      delegate_to: localhost
 
    - name: Apply security patches (RHEL/CentOS)
      dnf:
        name: "*"
        state: latest
        security: yes
        update_cache: yes
      when: ansible_os_family == "RedHat"
      register: patch_result
 
    - name: Apply security patches (Ubuntu/Debian)
      apt:
        upgrade: dist
        update_cache: yes
        only_upgrade: yes
      when: ansible_os_family == "Debian"
      register: patch_result
 
    - name: Reboot if required
      reboot:
        reboot_timeout: 600
        msg: "Rebooting for patch installation"
      when: patch_result.changed
 
    - name: Run post-patch validation
      include_tasks: validate_services.yml
 
    - name: Report patch results
      debug:
        msg: "Patching {{ 'succeeded' if patch_result.changed else 'no updates' }} on {{ inventory_hostname }}"

Step 4: Production Deployment

# deploy_patches.yml - Phased production rollout
---
- name: Ring 1 - IT Early Adopters
  hosts: ring1_hosts
  serial: "25%"
  max_fail_percentage: 10
  become: yes
  tasks:
    - import_tasks: apply_patches.yml
    - import_tasks: validate_services.yml
    - name: Wait for soak period
      pause:
        hours: 48
      run_once: true
 
- name: Ring 2 - Business Pilot
  hosts: ring2_hosts
  serial: "20%"
  max_fail_percentage: 5
  become: yes
  tasks:
    - import_tasks: apply_patches.yml
    - import_tasks: validate_services.yml
 
- name: Ring 3 - General Deployment
  hosts: ring3_hosts
  serial: "10%"
  max_fail_percentage: 3
  become: yes
  tasks:
    - import_tasks: apply_patches.yml
    - import_tasks: validate_services.yml

Step 5: Verification and Reporting

Run a post-patch vulnerability scan to confirm patch installation:

# Trigger post-patch verification scan
curl -k -X POST "https://nessus:8834/scans/$VERIFY_SCAN_ID/launch" \
  -H "X-Cookie: token=$TOKEN"
 
# Compare pre-patch and post-patch results
# Expecting reduction in vulnerabilities matching deployed patches

Patch Management SLAs

Severity SLA (Internet-Facing) SLA (Internal) SLA (Air-Gapped)
Critical (CVSS 9+) 48 hours 7 days 14 days
High (CVSS 7-8.9) 7 days 14 days 30 days
Medium (CVSS 4-6.9) 30 days 30 days 60 days
Low (CVSS 0.1-3.9) 90 days 90 days 90 days

Best Practices

  1. Maintain current asset inventory to ensure complete patch coverage
  2. Test all patches in a non-production environment before deployment
  3. Use phased rollouts with automatic rollback capabilities
  4. Coordinate patch windows with change management process
  5. Track patch compliance metrics and report to leadership
  6. Automate where possible to reduce manual effort and human error
  7. Maintain exception documentation for systems that cannot be patched
  8. Include third-party application patching (not just OS patches)

Common Pitfalls

  • Patching only operating systems and ignoring third-party applications
  • No rollback plan if patches cause service disruption
  • Treating all patches with equal urgency (no risk-based prioritization)
  • Manual patch processes that cannot scale
  • No post-patch verification to confirm successful installation
  • Ignoring firmware and BIOS updates
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 3

api-reference.md4.5 KB

API Reference: Patch Management Workflow Automation

Libraries Used

Library Purpose
requests HTTP client for Tenable.io, Qualys, and WSUS APIs
json Parse scan and patch compliance data
csv Export remediation plans to CSV
subprocess Execute PowerShell WSUS commands
os Read API credentials from environment

Installation

pip install requests

Tenable.io API

Authentication

import requests
import os
 
TENABLE_URL = "https://cloud.tenable.com"
tenable_headers = {
    "X-ApiKeys": f"accessKey={os.environ['TENABLE_ACCESS_KEY']};secretKey={os.environ['TENABLE_SECRET_KEY']}",
    "Content-Type": "application/json",
}

Key Endpoints

Method Endpoint Description
GET /scans List vulnerability scans
GET /scans/{id} Get scan results
POST /scans Create a new scan
POST /scans/{id}/launch Launch a scan
GET /workbenches/vulnerabilities List vulnerabilities
GET /workbenches/assets List assets

Get Scan Results with Missing Patches

def get_tenable_missing_patches(scan_id):
    resp = requests.get(
        f"{TENABLE_URL}/scans/{scan_id}",
        headers=tenable_headers,
        timeout=60,
    )
    resp.raise_for_status()
    vulns = resp.json().get("vulnerabilities", [])
    patches_needed = [
        v for v in vulns
        if v.get("plugin_family") == "Windows : Microsoft Bulletins"
        or "patch" in v.get("plugin_name", "").lower()
    ]
    return sorted(patches_needed, key=lambda v: v.get("severity", 0), reverse=True)

Qualys API

Authentication

QUALYS_URL = os.environ.get("QUALYS_URL", "https://qualysapi.qualys.com")
qualys_auth = (os.environ["QUALYS_USER"], os.environ["QUALYS_PASS"])
qualys_headers = {"X-Requested-With": "Python"}

Key Endpoints

Method Endpoint Description
GET /api/2.0/fo/scan/ List scans
POST /api/2.0/fo/scan/ Launch a scan
GET /api/2.0/fo/asset/host/ List host assets
POST /api/2.0/fo/knowledge_base/vuln/ Query vulnerability KB
GET /api/2.0/fo/report/ List reports

Get Missing Patches by Host

def get_qualys_patches(scan_ref):
    resp = requests.get(
        f"{QUALYS_URL}/api/2.0/fo/scan/",
        params={"action": "fetch", "scan_ref": scan_ref, "output_format": "json"},
        auth=qualys_auth,
        headers=qualys_headers,
        timeout=120,
    )
    resp.raise_for_status()
    return resp.json()

WSUS (Windows Server Update Services) via PowerShell

Check Patch Compliance

def check_wsus_compliance(server=None):
    cmd = ["powershell", "-Command"]
    ps_script = """
    Get-WsusUpdate -Approval Approved -Status FailedOrNeeded |
    Select-Object Title, Classification, KnowledgebaseArticles, ArrivalDate |
    ConvertTo-Json
    """
    if server:
        ps_script = f"Invoke-Command -ComputerName {server} -ScriptBlock {{{ps_script}}}"
    cmd.append(ps_script)
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
    return json.loads(result.stdout) if result.stdout else []

List Installed Updates

def list_installed_patches():
    cmd = [
        "powershell", "-Command",
        "Get-HotFix | Select-Object HotFixID, Description, InstalledOn | ConvertTo-Json"
    ]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
    return json.loads(result.stdout) if result.stdout else []

Patch Prioritization

def prioritize_patches(vulnerabilities, kev_cves=None):
    """Prioritize patches using CVSS + KEV + age."""
    kev_set = set(kev_cves or [])
    for vuln in vulnerabilities:
        score = vuln.get("cvss_score", 0)
        if vuln.get("cve") in kev_set:
            score += 3  # KEV bonus
        if vuln.get("exploit_available"):
            score += 2
        vuln["priority_score"] = min(score, 10)
    return sorted(vulnerabilities, key=lambda v: v["priority_score"], reverse=True)

Output Format

{
  "scan_date": "2025-01-15T10:30:00Z",
  "total_hosts": 250,
  "patches_required": 145,
  "critical_patches": 12,
  "kev_matches": 5,
  "compliance_rate": 78.5,
  "remediation_plan": [
    {
      "kb": "KB5034441",
      "title": "Windows Security Update",
      "severity": "critical",
      "affected_hosts": 45,
      "cve": "CVE-2024-21345",
      "kev_listed": true
    }
  ]
}
standards.md1.5 KB

Standards and References - Patch Management Workflow

Industry Standards

  • NIST SP 800-40 Rev 4: Guide to Enterprise Patch Management Planning
  • NIST SP 800-53 SI-2: Flaw Remediation control
  • CIS Controls v8 Control 7.3: Perform automated patch management
  • PCI DSS v4.0 Req 6.3: Identify and address security vulnerabilities
  • ISO 27001:2022 A.8.8: Management of technical vulnerabilities

Patch Management Tools

Tool Platform Type License
WSUS Windows Microsoft native Free with Windows Server
SCCM/MECM Windows/Linux Enterprise endpoint management Microsoft licensing
Ansible Linux/Windows Agentless automation Open source / Red Hat
Intune Windows/macOS/iOS/Android Cloud MDM/MAM Microsoft 365
Jamf Pro macOS/iOS Apple device management Commercial
Ivanti Patch Multi-platform Enterprise patching Commercial
ManageEngine Multi-platform IT management suite Commercial

Vendor Patch Schedules

Vendor Schedule Source
Microsoft Second Tuesday monthly https://msrc.microsoft.com/update-guide
Adobe Second Tuesday monthly https://helpx.adobe.com/security/products.html
Oracle Quarterly (Jan, Apr, Jul, Oct) https://www.oracle.com/security-alerts/
Cisco As needed https://sec.cloudapps.cisco.com/security/center
Linux distributions Continuous Distribution-specific advisories
workflows.md2.4 KB

Workflows - Patch Management

Workflow 1: End-to-End Patch Lifecycle

┌────────────┐   ┌──────────┐   ┌──────────────┐   ┌──────────┐
│  Discover  │──>│  Assess  │──>│  Prioritize  │──>│   Test   │
│  (Vendor   │   │  (CVE    │   │  (CVSS+EPSS  │   │  (Lab    │
│   Feeds)   │   │  Match)  │   │   Scoring)   │   │  Ring 0) │
└────────────┘   └──────────┘   └──────────────┘   └──────────┘

    ┌───────────────────────────────────────────────────┘
    v
┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│ Approve  │──>│  Deploy  │──>│  Verify  │──>│  Report  │
│ (CAB /   │   │ (Phased  │   │ (Re-scan │   │ (Metrics │
│  Change) │   │  Rings)  │   │  Confirm)│   │  + KPIs) │
└──────────┘   └──────────┘   └──────────┘   └──────────┘

Workflow 2: Emergency Patch Process

For critical zero-day or actively exploited vulnerabilities:

  1. Alert (T+0h): Vendor advisory or threat intel notification
  2. Triage (T+1h): Assess applicability and impact
  3. Fast-track Test (T+4h): Rapid testing on critical systems
  4. Emergency CAB (T+6h): Expedited approval
  5. Deploy (T+8h): Direct to production (skip pilot rings)
  6. Verify (T+12h): Post-patch scan verification
  7. Post-mortem (T+48h): Review process effectiveness

Workflow 3: Rollback Procedure

Patch Deployment Fails

    ├──> Application Not Starting
    │       └──> Restore from snapshot/backup

    ├──> Performance Degradation
    │       └──> Uninstall patch (wusa /uninstall /kb:NNNNN)

    ├──> Blue Screen / Kernel Panic
    │       └──> Boot to safe mode, remove update

    └──> Network Connectivity Lost
            └──> Console access, rollback patch

Scripts 2

agent.py9.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Patch management workflow agent.

Audits system patch compliance by checking installed package versions
against known vulnerabilities, tracking patch SLA adherence, and
generating remediation reports. Supports Linux (apt/yum) and basic
CVE cross-referencing via the CISA KEV catalog.
"""
import argparse
import json
import os
import subprocess
import sys
from datetime import datetime, timezone, timedelta

try:
    import requests
except ImportError:
    requests = None


def check_apt_updates():
    """Check for available security updates on Debian/Ubuntu systems."""
    findings = []
    print("[*] Checking apt security updates...")

    # Update package lists
    result = subprocess.run(
        ["apt-get", "update", "-qq"],
        capture_output=True, text=True, timeout=120,
    )

    # List upgradable packages
    result = subprocess.run(
        ["apt", "list", "--upgradable"],
        capture_output=True, text=True, timeout=60,
    )
    if result.returncode != 0:
        return [{"check": "apt updates", "status": "ERROR", "severity": "HIGH",
                 "detail": result.stderr[:200]}]

    for line in result.stdout.strip().splitlines():
        if "Listing..." in line:
            continue
        parts = line.split("/")
        if len(parts) >= 2:
            pkg_name = parts[0]
            is_security = "security" in line.lower()
            version_info = line.split()
            current = version_info[-1] if len(version_info) > 3 else "unknown"
            available = version_info[1] if len(version_info) > 1 else "unknown"
            findings.append({
                "package": pkg_name,
                "current_version": current,
                "available_version": available,
                "is_security_update": is_security,
                "severity": "CRITICAL" if is_security else "MEDIUM",
            })

    security_count = sum(1 for f in findings if f.get("is_security_update"))
    print(f"[+] Found {len(findings)} updates ({security_count} security)")
    return findings


def check_yum_updates():
    """Check for available security updates on RHEL/CentOS systems."""
    findings = []
    print("[*] Checking yum/dnf security updates...")

    for pkg_mgr in ["dnf", "yum"]:
        result = subprocess.run(
            [pkg_mgr, "check-update", "--security", "-q"],
            capture_output=True, text=True, timeout=120,
        )
        if result.returncode in (0, 100):
            break
    else:
        return [{"check": "yum/dnf", "status": "ERROR", "severity": "HIGH",
                 "detail": "Neither yum nor dnf available"}]

    for line in result.stdout.strip().splitlines():
        parts = line.split()
        if len(parts) >= 3:
            findings.append({
                "package": parts[0],
                "available_version": parts[1],
                "repository": parts[2] if len(parts) > 2 else "",
                "is_security_update": True,
                "severity": "HIGH",
            })

    print(f"[+] Found {len(findings)} security updates")
    return findings


def check_windows_updates():
    """Check for pending Windows updates via PowerShell."""
    findings = []
    print("[*] Checking Windows Update status...")

    ps_script = (
        "Get-HotFix | Sort-Object InstalledOn -Descending | "
        "Select-Object -First 20 HotFixID, Description, InstalledOn | "
        "ConvertTo-Json"
    )
    result = subprocess.run(
        ["powershell", "-Command", ps_script],
        capture_output=True, text=True, timeout=120,
    )
    if result.returncode == 0 and result.stdout.strip():
        try:
            hotfixes = json.loads(result.stdout)
            if isinstance(hotfixes, dict):
                hotfixes = [hotfixes]
            for hf in hotfixes:
                findings.append({
                    "hotfix_id": hf.get("HotFixID", ""),
                    "description": hf.get("Description", ""),
                    "installed_on": str(hf.get("InstalledOn", "")),
                    "status": "installed",
                })
            if hotfixes:
                latest = hotfixes[0]
                installed_date = latest.get("InstalledOn", "")
                print(f"[+] Latest patch: {latest.get('HotFixID', 'N/A')} ({installed_date})")
        except json.JSONDecodeError:
            pass

    return findings


def check_kev_exposure(package_cves):
    """Cross-reference package CVEs against CISA KEV catalog."""
    if not requests:
        return []

    kev_url = "https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json"
    try:
        resp = requests.get(kev_url, timeout=30)
        resp.raise_for_status()
        kev_data = resp.json()
        kev_cves = {v["cveID"] for v in kev_data.get("vulnerabilities", [])}
    except Exception:
        return []

    exposed = []
    for cve_id in package_cves:
        if cve_id.upper() in kev_cves:
            exposed.append({
                "cve_id": cve_id,
                "in_kev": True,
                "severity": "CRITICAL",
                "description": "Actively exploited vulnerability (CISA KEV)",
            })
    return exposed


def assess_patch_sla(findings, sla_days=None):
    """Assess patch compliance against SLA targets."""
    if sla_days is None:
        sla_days = {"CRITICAL": 7, "HIGH": 30, "MEDIUM": 90, "LOW": 180}

    sla_findings = []
    for f in findings:
        severity = f.get("severity", "MEDIUM")
        target_days = sla_days.get(severity, 90)
        sla_findings.append({
            "package": f.get("package", f.get("hotfix_id", "unknown")),
            "severity": severity,
            "sla_target_days": target_days,
            "in_sla": True,  # Would need install date to determine
            "recommendation": f"Patch within {target_days} days per SLA policy",
        })
    return sla_findings


def format_summary(findings, kev_findings, sla_findings, platform):
    """Print patch management summary."""
    print(f"\n{'='*60}")
    print(f"  Patch Management Audit Report")
    print(f"{'='*60}")
    print(f"  Platform        : {platform}")
    print(f"  Pending Updates : {len(findings)}")

    security = sum(1 for f in findings if f.get("is_security_update"))
    print(f"  Security Updates: {security}")
    print(f"  KEV Matches     : {len(kev_findings)}")

    severity_counts = {}
    for f in findings:
        sev = f.get("severity", "MEDIUM")
        severity_counts[sev] = severity_counts.get(sev, 0) + 1

    print(f"\n  By Severity:")
    for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW"]:
        count = severity_counts.get(sev, 0)
        if count > 0:
            print(f"    {sev:10s}: {count}")

    if kev_findings:
        print(f"\n  CISA KEV Exposed CVEs (IMMEDIATE ACTION):")
        for k in kev_findings:
            print(f"    {k['cve_id']}: {k['description']}")

    if findings:
        print(f"\n  Pending Updates:")
        for f in findings[:20]:
            pkg = f.get("package", f.get("hotfix_id", "unknown"))
            sev = f.get("severity", "MEDIUM")
            sec = " [SECURITY]" if f.get("is_security_update") else ""
            print(f"    [{sev:8s}] {pkg}{sec}")

    return severity_counts


def main():
    parser = argparse.ArgumentParser(
        description="Patch management workflow audit agent"
    )
    parser.add_argument("--platform", choices=["auto", "apt", "yum", "windows"],
                        default="auto", help="Package manager to check")
    parser.add_argument("--cves", nargs="+", help="CVE IDs to check against KEV")
    parser.add_argument("--sla-critical", type=int, default=7, help="SLA days for critical (default: 7)")
    parser.add_argument("--sla-high", type=int, default=30, help="SLA days for high (default: 30)")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    findings = []
    platform = args.platform

    if platform == "auto":
        if sys.platform == "win32":
            platform = "windows"
        elif os.path.isfile("/usr/bin/apt"):
            platform = "apt"
        elif os.path.isfile("/usr/bin/yum") or os.path.isfile("/usr/bin/dnf"):
            platform = "yum"
        else:
            print("[!] Could not detect package manager", file=sys.stderr)
            sys.exit(1)

    if platform == "apt":
        findings = check_apt_updates()
    elif platform == "yum":
        findings = check_yum_updates()
    elif platform == "windows":
        findings = check_windows_updates()

    kev_findings = []
    if args.cves:
        kev_findings = check_kev_exposure(args.cves)

    sla_days = {"CRITICAL": args.sla_critical, "HIGH": args.sla_high, "MEDIUM": 90, "LOW": 180}
    sla_findings = assess_patch_sla(findings, sla_days)

    severity_counts = format_summary(findings, kev_findings, sla_findings, platform)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "Patch Management Audit",
        "platform": platform,
        "pending_updates": findings,
        "kev_exposure": kev_findings,
        "sla_assessment": sla_findings,
        "severity_counts": severity_counts,
        "risk_level": (
            "CRITICAL" if kev_findings or severity_counts.get("CRITICAL", 0) > 0
            else "HIGH" if severity_counts.get("HIGH", 0) > 0
            else "MEDIUM" if findings
            else "LOW"
        ),
    }

    if args.output:
        with open(args.output, "w") as f:
            json.dump(report, f, indent=2)
        print(f"\n[+] Report saved to {args.output}")
    elif args.verbose:
        print(json.dumps(report, indent=2))


if __name__ == "__main__":
    main()
process.py13.7 KB
Display-only source. This catalog never executes bundled scripts.
#!/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"""<!DOCTYPE html>
<html>
<head>
    <title>Patch Compliance Report - {datetime.now().strftime('%Y-%m-%d')}</title>
    <style>
        body {{ font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }}
        .header {{ background: #1a1a2e; color: white; padding: 20px; border-radius: 8px; }}
        .metrics {{ display: flex; gap: 15px; margin: 20px 0; flex-wrap: wrap; }}
        .card {{ background: white; padding: 20px; border-radius: 8px; flex: 1; min-width: 200px;
                 box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }}
        .card h3 {{ margin: 0; font-size: 2em; }}
        .card p {{ margin: 5px 0 0; color: #666; }}
        .green {{ border-top: 4px solid #27ae60; }}
        .red {{ border-top: 4px solid #e74c3c; }}
        .orange {{ border-top: 4px solid #e67e22; }}
        table {{ width: 100%; border-collapse: collapse; background: white; margin: 15px 0;
                 border-radius: 8px; overflow: hidden; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }}
        th {{ background: #2c3e50; color: white; padding: 12px; text-align: left; }}
        td {{ padding: 10px 12px; border-bottom: 1px solid #eee; }}
    </style>
</head>
<body>
    <div class="header">
        <h1>Patch Compliance Report</h1>
        <p>Generated: {datetime.now().strftime('%Y-%m-%d %H:%M')}</p>
    </div>
    <div class="metrics">
        <div class="card green"><h3>{summary['compliance_rate']}</h3><p>Compliance Rate</p></div>
        <div class="card orange"><h3>{summary['total_hosts']}</h3><p>Total Hosts</p></div>
        <div class="card red"><h3>{summary['total_missing_patches']}</h3><p>Missing Patches</p></div>
        <div class="card red"><h3>{summary['critical_patches_missing']}</h3><p>Critical Missing</p></div>
    </div>

    <h2>Compliance by Tier</h2>
    <table>
        <tr><th>Tier</th><th>Total</th><th>Compliant</th><th>Rate</th><th>Avg Score</th></tr>
        {''.join(f"<tr><td>{tier}</td><td>{data['total']}</td><td>{data['compliant']}</td><td>{data['rate']}</td><td>{data['avg_score']}</td></tr>" for tier, data in summary['by_tier'].items())}
    </table>

    <h2>Top Non-Compliant Hosts</h2>
    <table>
        <tr><th>Hostname</th><th>Tier</th><th>OS</th><th>Critical</th><th>High</th><th>Medium</th><th>Score</th></tr>
        {''.join(f"<tr><td>{r.hostname}</td><td>{r.tier}</td><td>{r.os}</td><td>{r.critical_missing}</td><td>{r.high_missing}</td><td>{r.medium_missing}</td><td>{r.compliance_score}</td></tr>" for r in top_noncompliant.itertuples())}
    </table>
</body>
</html>"""

    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()

Assets 1

template.mdtext/markdown · 1.1 KB
Keep exploring