Sign in

Patching of Azure Environment

There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

This Runbook uses Azure Update Manager and automates updates and patches for VMs across Azure. It provides scheduling, deploying, and tracking of update compliance, reducing management overhead. This ensures VMs stay protected against vulnerabilities with minimal service impact.

  1. 1

    Get VMs part of resource group or specified list.

    There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

    This task involves listing all Azure VMs across all resource groups, enabling centralized management and oversight.

    import json def process_vm_data(vm_data): processed_vms = [] for vm in vm_data: vm_details = { 'vm_name': vm['name'], 'resource_group': vm['resourceGroup'], 'location': vm['location'], 'vm_size': vm['hardwareProfile']['vmSize'], 'os_type': vm['storageProfile']['osDisk']['osType'], 'os_disk_name': vm['storageProfile']['osDisk']['name'], 'os_disk_id': vm['storageProfile']['osDisk']['managedDisk']['id'] } processed_vms.append(vm_details) return processed_vms try: result = _exe(None, "az vm list") #print(result) op = json.loads(result) #print(json.dumps(op,indent=4)) # for debugging # Processing the VM data processed_vms = process_vm_data(op) #print(processed_vms) # for debugging # Printing the processed VM details for downstream tasks for vm in processed_vms: #print(json.dumps(vm, indent=4)) print("VM Details") print("==========") print(f"VM Name: {vm['vm_name']}") print(f"Resource Group: {vm['resource_group']}") print(f"Location: {vm['location']}") print(f"VM Size: {vm['vm_size']}") print(f"OS Type: {vm['os_type']}") print(f"OS Disk Name: {vm['os_disk_name']}") print(f"OS Disk ID: {vm['os_disk_id']}") print("-" * 30) # Separator for readability between VMs except json.JSONDecodeError: print("Error decoding JSON response from Azure CLI.")
    copied
    1
  2. 2

    Collect list of patches to be deployed.

    There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

    This task involves fetching a comprehensive list of patches awaiting deployment on Azure Virtual Machines. This ensures the VMs are up-to-date with the latest security and performance improvements.

    import json def assess_patches_for_vms(processed_vms): patches_list = [] for vm in processed_vms: vm_name = vm['vm_name'] resource_group = vm['resource_group'] # Constructing the Azure CLI command command = f"az vm assess-patches -g {resource_group} -n {vm_name}" try: # Execute the command result = _exe(None, command) patches_info = json.loads(result) #print(f"Raw Patches for {vm_name}: {patches_info}") # Store the patches info with VM name patches_list.append({ 'vm_name': vm_name, 'patches_info': patches_info }) print(f"Patches assessed for VM: {vm_name}") print("=" * 50) except json.JSONDecodeError: print(f"Error decoding JSON response for VM: {vm_name} as the VM maybe stopped or deallocated") except Exception as e: print(f"An error occurred while assessing patches for VM: {vm_name}: {str(e)}") return patches_list # Example VM details obtained from previous steps #processed_vms = [{'vm_name': 'test-update-manager', 'resource_group': 'DEFAULTRESOURCEGROUP-EUS', 'location': 'eastus', 'vm_size': 'Standard_B1s', 'os_type': 'Linux', 'os_disk_name': 'test-update-manager_disk1_9ca2728077904a74a8a09a9d40efc938', 'os_disk_id': '/subscriptions/955ecf93-74f8-4728-bd2a-31094aa55629/resourceGroups/DEFAULTRESOURCEGROUP-EUS/providers/Microsoft.Compute/disks/test-update-manager_disk1_9ca2728077904a74a8a09a9d40efc938'}, {'vm_name': 'test-update-manager-customer-managed', 'resource_group': 'RG-VMINSTANCES-EASTUS', 'location': 'eastus', 'vm_size': 'Standard_D2s_v3', 'os_type': 'Linux', 'os_disk_name': 'test-update-manager-customer-managed_disk1_8db9f76765ff459c8084f5949c4fd8aa', 'os_disk_id': '/subscriptions/955ecf93-74f8-4728-bd2a-31094aa55629/resourceGroups/rg-vminstances-eastus/providers/Microsoft.Compute/disks/test-update-manager-customer-managed_disk1_8db9f76765ff459c8084f5949c4fd8aa'}] patches_list = assess_patches_for_vms(processed_vms) # Iterate through each VM's patch list for item in patches_list: print(f"VM Name: {item['vm_name']}") data = item['patches_info'] # This is where the patch information is stored. # Display the overall assessment status and patch counts. print(f"Assessment Status: {data['status']}") print(f"Critical and Security Patch Count: {data['criticalAndSecurityPatchCount']}") print(f"Other Patch Count: {data['otherPatchCount']}") print(f"Reboot Pending: {'Yes' if data['rebootPending'] else 'No'}") print("-" * 50) # Display detailed information for each available patch. for patch in data["availablePatches"]: print(f"Name: {patch['name']}") print(f"KBID: {patch['kbId']}") # Added KBID as it seems important. print(f"Classification: {', '.join(patch['classifications'])}") print(f"Reboot Behavior: {patch['rebootBehavior']}") print(f"Last Modified: {patch['lastModifiedDateTime']}") print(f"Published Date: {patch.get('publishedDate', 'N/A')}") # Using .get in case the key doesn't exist. print("-" * 50) ''' # Example: Printing the patches list for item in patches_list: print(json.dumps(item, indent=4)) ''' #context.proceed=False
    copied
    2
  3. 3

    Review latest available patches on Servers.

    There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

    This task reviews the latest installed patches on servers and involves checking and documenting the most recent updates applied to ensure systems are current and secure.

    import json from datetime import datetime def get_available_updates(): ps_command = "Get-HotFix | Select-Object -Property Description, HotFixID, InstalledOn | ConvertTo-Json" result = _exe(hostname, ps_command) try: updates = json.loads(result) return updates except json.JSONDecodeError: print("Failed to decode JSON from PowerShell output.") return [] def format_update_details(updates): formatted_updates = [] for update in updates: # Convert /Date(1712448000000)/ format to a human-readable date if needed installed_on = datetime.fromtimestamp( int(update["InstalledOn"]["value"][6:-2]) / 1000 # Extract timestamp and convert to seconds ).strftime('%Y-%m-%d %H:%M:%S') formatted_updates.append({ "Description": update["Description"], "HotFixID": update["HotFixID"], "InstalledOn": installed_on }) return formatted_updates available_updates = get_available_updates() formatted_updates = format_update_details(available_updates) print("Available updates:") for update in formatted_updates: print(f"HotFixID: {update['HotFixID']}, Description: {update['Description']}, Installed On: {update['InstalledOn']}")
    copied
    3
  4. 4

    Enable Scheduled Patching for Azure VM

    There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

    This task involves setting "patchMode" to "AutomaticByPlatform" and "bypassPlatformSafetyChecksOnUserSchedule" to True. This configures VMs for automatic updates on a customer managed schedule(a prerequisite to enable scheduled patching successfully).

    import json def extract_vm_names_and_resource_groups(processed_vms): extracted_details = [] for vm in processed_vms: # Directly extract 'vm_name' and 'resource_group' from each dictionary vm_name = vm.get("vm_name") resource_group = vm.get("resource_group") extracted_details.append((vm_name, resource_group)) return extracted_details def construct_and_execute_patch_commands(subscription_id, resource_groups, vm_names, api_version, body_dict): if len(vm_names) != len(resource_groups): print("Error: The lengths of vm_names and resource_groups do not match.") return # Convert the Python dictionary to a JSON string body_json = json.dumps(body_dict, ensure_ascii=False).replace('True', 'true').replace('False', 'false') # Correctly replace Python True to JSON true for vm_name, resource_group in zip(vm_names, resource_groups): url = f"https://management.azure.com/subscriptions/{subscription_id}/resourceGroups/{resource_group}/providers/Microsoft.Compute/virtualMachines/{vm_name}?api-version={api_version}" # Construct the full command. Note: For actual execution, consider splitting and escaping correctly. command = f"""az rest --method patch --url '{url}' --body '{body_json}'""" print(f"Executing patch command for VM: {vm_name} in resource_group: {resource_group}") result = _exe(None, command) #print(result) print(f"Enabled Scheduled Patching(Customer Managed Schedules) for Azure VM: {vm_name}") # Extract VM names and resource groups extracted_vm_details = extract_vm_names_and_resource_groups(processed_vms) #print(extracted_vm_details) api_version = "2024-03-01" body_dict = { "properties": { "osProfile": { "windowsConfiguration": { "provisionVMAgent": True, "enableAutomaticUpdates": True, "patchSettings": { "patchMode": "AutomaticByPlatform", "automaticByPlatformSettings": { "bypassPlatformSafetyChecksOnUserSchedule": True } } } } } } # Unpack the extracted VM details into separate lists vm_names, resource_groups = zip(*extracted_vm_details) # Execute patch commands for extracted VMs construct_and_execute_patch_commands(subscription_id, list(resource_groups), list(vm_names), api_version, body_dict)
    copied
    4
  5. 5

    Deploy patches on Servers.

    There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

    This task involves assessing and applying the latest security and performance updates to protect and optimize the Azure VMs using Azure Update Manager.

    import json ''' [1] Install patches on a windows VM, allowing the maximum amount of time to be 4 hours, and the VM will reboot if required during the software update operation. az vm install-patches -g MyResourceGroup -n MyVm --maximum-duration PT4H --reboot-setting IfRequired --classifications-to-include-win Critical Security --exclude-kbs-requiring-reboot true [2] Install patches on a linux VM, allowing the maximum amount of time to be 4 hours, and the VM will reboot if required during the software update operation. az vm install-patches -g MyResourceGroup -n MyVm --maximum-duration PT4H --reboot-setting IfRequired --classifications-to-include-linux Critical az vm install-patches --maximum-duration --reboot-setting {Always, IfRequired, Never} [--classifications-to-include-linux {Critical, Other, Security}] [--classifications-to-include-win {Critical, Definition, FeaturePack, Security, ServicePack, Tools, UpdateRollUp, Updates}] [--exclude-kbs-requiring-reboot {false, true}] [--ids] [--kb-numbers-to-exclude] [--kb-numbers-to-include] [--name] [--no-wait] [--package-name-masks-to-exclude] [--package-name-masks-to-include] [--resource-group] [--subscription]''' def install_critical_and_security_patches(vm_details): patch_installation_results = [] for vm in vm_details: vm_name = vm['vm_name'] resource_group = vm['resource_group'] # Specify the classifications to include # classifications = "UpdateRollUp" # Adjust as needed: "Security", "Definition", "Updates", "UpdateRollUp","Other" etc. # Form the command with the correct parameters for classifications command = f"""az vm install-patches -g '{resource_group}' -n '{vm_name}' --maximum-duration PT4H --reboot-setting IfRequired --classifications-to-include-win '{classifications}' --kb-numbers-to-exclude '{kb_number_to_exclude}'""" # Form the command with the correct parameters for #command = f"""az vm install-patches -g '{resource_group}' -n '{vm_name}' --maximum-duration PT4H --reboot-setting IfRequired --kb-numbers-to-include {kb_number_to_include} --exclude-kbs-requiring-reboot false""" print(command) try: # Execute the command and capture the result result = _exe(None, command) print(f"Patches installation initiated for VM: {vm_name} in resource group: {resource_group}.") #print(type(result)) #for debugging lines = result.split("\n") filtered_lines = [x for x in lines if "WARNING" not in x] result = "\n".join(filtered_lines) #print(result) # for debugging (Success/Error Message of install-patches command for a VM) #print(f"### Server Compliance Report for Azure VM: {vm_name}\n") try: # Assume result is a JSON string; parse it into a Python dictionary result_data = json.loads(result) #generate_compliance_report(result_data) # Store the result with VM details patch_installation_results.append({ 'vm_name': vm_name, 'resource_group': resource_group, 'installation_result': result_data }) except json.JSONDecodeError as json_err: print(f"JSON parsing error: {json_err}. Raw result: {result}") except Exception as e: print(f"Failed to initiate patch installation for VM: {vm_name} in resource group: {resource_group}. Error: {str(e)}") # Return the list of results return patch_installation_results # processed_vms to be received from upstream task results = install_critical_and_security_patches(processed_vms)
    copied
    5
  6. 6

    Validate the patch installation.

    There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

    The Servers Patch Installation Run Report provides detailed insights into the patching process for each server, showcasing which patches were installed, failed, or are pending, along with the status of each installation attempt.

    import json def generate_compliance_report(patch_installation_results): print("### Server Patch Installation Run Report\n") for server in patch_installation_results: vm_name = server['vm_name'] resource_group = server['resource_group'] patch_result = server['installation_result'] print(f"## Server: {vm_name} in Resource Group: {resource_group}\n") print("#### General Information") print(f"- **Assessment Status**: {patch_result['status']}") print(f"- **Installation Activity ID**: {patch_result['installationActivityId']}") print(f"- **Assessment Start Time**: {patch_result['startDateTime']}") print(f"- **Reboot Status**: {patch_result['rebootStatus']}\n") print("#### Patch Summary") print(f"- **Installed Patches**: {patch_result['installedPatchCount']}") print(f"- **Failed Patches**: {patch_result['failedPatchCount']}") print(f"- **Not Selected Patches**: {patch_result['notSelectedPatchCount']}") print(f"- **Pending Patches**: {patch_result['pendingPatchCount']}") print(f"- **Excluded Patches**: {patch_result['excludedPatchCount']}") print(f"- **Maintenance Window Exceeded**: {'Yes' if patch_result['maintenanceWindowExceeded'] else 'No'}\n") print("### Detailed Patch Information") print("The following patches were assessed for installation:\n") for patch in patch_result["patches"]: print(f" **{patch['name']}**") print(f" - **Classification**: {', '.join(patch['classifications'])}") print(f" - **Installation State**: {patch['installationState']}") print(f" - **KB ID**: {patch['kbId']}\n") print("### Action Items and Recommendations") print("- **Review Not Selected Patches**: Review the patches not selected for installation.") print("- **Monitor Installed Patches**: Ensure the installed patches do not cause issues.") print("-"*50) # Separator between servers #print(results) # Example results below # results = [{'vm_name': 'windows-server-vm-2', 'resource_group': 'DEFAULT-EASTUS-VM-2', 'installation_result': {'error': {'code': 'Success', 'details': [], 'innererror': None, 'message': '0 error/s reported', 'target': None}, 'excludedPatchCount': 1, 'failedPatchCount': 0, 'installationActivityId': 'd5de8eaf-f64b-478c-9236-b47868be9cf1', 'installedPatchCount': 0, 'maintenanceWindowExceeded': False, 'notSelectedPatchCount': 3, 'patches': [{'classifications': ['UpdateRollUp'], 'installationState': 'NotSelected', 'kbId': '890830', 'name': 'Windows Malicious Software Removal Tool x64 - v5.123 (KB890830)', 'patchId': '3a7c8e27-21f7-4808-b15b-e5a719b84aad', 'version': None}, {'classifications': ['Security'], 'installationState': 'NotSelected', 'kbId': '5037034', 'msrcSeverity': 'Important', 'name': '2024-04 Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64 (KB5037034)', 'patchId': '8a9ba144-4d65-4534-8492-6e303a6e888c', 'version': None}, {'classifications': ['Definition'], 'installationState': 'Excluded', 'kbId': '2267602', 'name': 'Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.409.160.0) - Current Channel (Broad)', 'patchId': '9734ecf1-eae8-47df-987e-f929bed10354', 'version': None}, {'classifications': ['Security'], 'installationState': 'NotSelected', 'kbId': '5036896', 'name': '2024-04 Cumulative Update for Windows Server 2019 (1809) for x64-based Systems (KB5036896)', 'patchId': '7cd1ae7a-88d9-45ea-9c71-6b06dd0d25fe', 'version': None}], 'pendingPatchCount': 0, 'rebootStatus': 'NotNeeded', 'startDateTime': '2024-04-10T06:03:41+00:00', 'status': 'Succeeded'}}, {'vm_name': 'windows-server-vm', 'resource_group': 'DEFAULT-EASTUS-VM', 'installation_result': {'error': {'code': 'Success', 'details': [], 'innererror': None, 'message': '0 error/s reported', 'target': None}, 'excludedPatchCount': 1, 'failedPatchCount': 0, 'installationActivityId': '3ca6b219-98b6-49ba-8b83-0663f1183959', 'installedPatchCount': 0, 'maintenanceWindowExceeded': False, 'notSelectedPatchCount': 3, 'patches': [{'classifications': ['UpdateRollUp'], 'installationState': 'NotSelected', 'kbId': '890830', 'name': 'Windows Malicious Software Removal Tool x64 - v5.123 (KB890830)', 'patchId': '3a7c8e27-21f7-4808-b15b-e5a719b84aad', 'version': None}, {'classifications': ['Security'], 'installationState': 'NotSelected', 'kbId': '5037034', 'msrcSeverity': 'Important', 'name': '2024-04 Cumulative Update for .NET Framework 3.5, 4.7.2 and 4.8 for Windows Server 2019 for x64 (KB5037034)', 'patchId': '8a9ba144-4d65-4534-8492-6e303a6e888c', 'version': None}, {'classifications': ['Definition'], 'installationState': 'Excluded', 'kbId': '2267602', 'name': 'Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.409.160.0) - Current Channel (Broad)', 'patchId': '9734ecf1-eae8-47df-987e-f929bed10354', 'version': None}, {'classifications': ['Security'], 'installationState': 'NotSelected', 'kbId': '5036896', 'name': '2024-04 Cumulative Update for Windows Server 2019 (1809) for x64-based Systems (KB5036896)', 'patchId': '7cd1ae7a-88d9-45ea-9c71-6b06dd0d25fe', 'version': None}], 'pendingPatchCount': 0, 'rebootStatus': 'NotNeeded', 'startDateTime': '2024-04-10T06:05:13+00:00', 'status': 'Succeeded'}}] generate_compliance_report(results)
    copied
    6
  7. 7

    Server Patch Compliance report.

    There was a problem that the LLM was not able to address. Please rephrase your prompt and try again.

    The Server Patch Compliance Report aggregates data from the last 30 days to display a comprehensive overview of patch installation statuses across all servers, utilizing a Kusto query for detailed and accurate retrieval of information.

    from datetime import datetime import json cmd="az graph query -q \"" cmd+="patchinstallationresources " cmd+="| where type =~ \\\"microsoft.compute/virtualmachines/patchinstallationresults\\\" or type =~ \\\"microsoft.hybridcompute/machines/patchinstallationresults\\\" " cmd+="| where properties.lastModifiedDateTime > ago(30d) " cmd+="| where properties.status in~ (\\\"Succeeded\\\",\\\"Failed\\\",\\\"CompletedWithWarnings\\\",\\\"InProgress\\\") " cmd+="| parse id with * 'achines/' resourceName '/patchInstallationResults/' * " cmd+="| project id, resourceName, type, properties.status, properties.startDateTime, properties.lastModifiedDateTime, properties.startedBy, properties, tags " cmd+="| union (" cmd+="patchassessmentresources " cmd+="| where type =~ \\\"microsoft.compute/virtualmachines/patchassessmentresults\\\" or type =~ \\\"microsoft.hybridcompute/machines/patchassessmentresults\\\" " cmd+="| where properties.lastModifiedDateTime > ago(30d) " cmd+="| where properties.status in~ (\\\"Succeeded\\\",\\\"Failed\\\",\\\"CompletedWithWarnings\\\",\\\"InProgress\\\") " cmd+="| parse id with * 'achines/' resourceName '/patchAssessmentResults/' * " cmd+="| project id, resourceName, type, properties.status, properties.startDateTime, properties.lastModifiedDateTime, properties.startedBy , properties, tags" cmd+=")\"" def parse_datetime(date_str): """Attempt to parse a datetime string first with milliseconds, then without.""" for fmt in ('%Y-%m-%dT%H:%M:%S.%fZ', '%Y-%m-%dT%H:%M:%SZ'): try: return datetime.strptime(date_str, fmt) except ValueError: pass raise ValueError(f"Time data {date_str} does not match any expected format.") def generate_patch_report(data): # Initialize table with the desired structure and headers table = context.newtable() table.title = "Patch Installation Runs History for the past 30 days" table.num_cols = 8 # Number of columns according to headers table.num_rows = 1 # Starts with one row for headers table.has_header_row = True # Define header names based on the new structure headers = ["Resource Name", "OS Type", "Status", "Last Modified", "Installed Patches", "Failed Patches", "Pending Patches", "Reboot Status"] # Set headers in the first row for col_num, header in enumerate(headers): table.setval(0, col_num, header) # Collect server data into a list for sorting server_data = [] for server in data.get("data", []): properties = server.get("properties", {}) server_info = { "resourceName": server.get("resourceName", "Unknown Resource"), "osType": properties.get("osType", "N/A"), "status": properties.get("status", "N/A"), "lastModifiedDateTime": properties.get("lastModifiedDateTime", "N/A"), "installedPatchCount": properties.get("installedPatchCount", 0), "failedPatchCount": properties.get("failedPatchCount", 0), "pendingPatchCount": properties.get("pendingPatchCount", 0), "rebootStatus": properties.get("rebootStatus", "N/A") } server_data.append(server_info) # Sort the collected data by "Last Modified Date" in descending order server_data.sort(key=lambda x: parse_datetime(x["lastModifiedDateTime"]), reverse=True) # Populate the table with sorted data for row_num, server in enumerate(server_data, start=1): # Starting from the second row table.num_rows += 1 # Add a row for each server values = [server["resourceName"], server["osType"], server["status"], server["lastModifiedDateTime"], str(server["installedPatchCount"]), str(server["failedPatchCount"]), str(server["pendingPatchCount"]), server["rebootStatus"]] for col_num, value in enumerate(values): table.setval(row_num, col_num, value) op = _exe(None,cmd) #print(op) # for raw results of the above query patch_compliance_table = generate_patch_report(json.loads(op))
    copied
    7