

Active exploitation of a Cisco ISE zero‑day has forced an urgent patch. The following guide shows developers how to automate version checks, patch uploads, and post‑patch verification using the Cisco ISE REST API. All code is ready‑to‑copy, includes error handling, and follows modern best practices.
<a name="step-1-prerequisites"></a>
| Item | Why you need it | Recommended version |
|---|---|---|
Cisco ISE admin account (with System Admin or Super Admin role) | Required to call the System/Update APIs | Any ISE 2.x/3.x that supports the REST API |
| Network access to the ISE Administration node (HTTPS 443) | The REST API is only exposed on the admin interface | – |
| Python 3.8+ | For the Python example | python --version |
| Node.js 14+ (or LTS) | For the JavaScript/TypeScript example | node --version |
| Git (optional) | To clone the sample repo | – |
| IDE / text editor | VS Code, PyCharm, etc. | – |
| cURL or Postman (for quick manual testing) | Helpful while debugging | – |
Note: The Cisco ISE REST API uses basic auth over HTTPS. If your deployment uses client‑certificate authentication, replace the auth block accordingly (see the “Common Patterns” section).
<a name="step-2-installation-and-setup"></a>
# 1️⃣ Create a virtual environment (recommended)
python -m venv venv
source venv/bin/activate # on Windows: venv\Scripts\activate
# 2️⃣ Install required packages
pip install --upgrade pip
pip install requests python-dotenv tenacity
# 1️⃣ Initialise a new project (if you don't have one)
mkdir ise-patch-tool && cd ise-patch-tool
npm init -y
# 2️⃣ Install dependencies
npm install axios dotenv
# For TypeScript support (optional but recommended)
npm install --save-dev typescript @types/node ts-node
npx tsc --init # creates tsconfig.json
<a name="step-3-basic-implementation"></a>
Below are complete, runnable scripts that:
/api/system/version).The scripts assume the patch file is a
.zipor.tar.gzthat Cisco provides via the PSIRT portal. Adjust thePATCH_FILE_PATHenv var to point to your local copy.
ise_patch.py)#!/usr/bin/env python3
"""
ise_patch.py – Automate Cisco ISE version check & emergency patch apply.
Requires:
- requests
- python-dotenv
- tenacity (for retry logic)
Environment variables (see .env example):
ISE_BASE_URL – e.g. https://ise.example.com
ISE_USERNAME – admin user
ISE_PASSWORD – password or API token
PATCH_FILE_PATH– absolute path to the patch bundle
TARGET_VERSION – desired version string (e.g. "3.1.0.212")
"""
import os
import sys
import time
import base64
from pathlib import Path
import requests
from dotenv import load_dotenv
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
# ----------------------------------------------------------------------
# Load configuration
# ----------------------------------------------------------------------
load_dotenv() # pulls variables from .env into os.environ
ISE_BASE_URL = os.getenv("ISE_BASE_URL").rstrip("/")
ISE_USERNAME = os.getenv("ISE_USERNAME")
ISE_PASSWORD = os.getenv("ISE_PASSWORD")
PATCH_FILE_PATH = os.getenv("PATCH_FILE_PATH")
TARGET_VERSION = os.getenv("TARGET_VERSION")
if not all([ISE_BASE_URL, ISE_USERNAME, ISE_PASSWORD, PATCH_FILE_PATH, TARGET_VERSION]):
sys.exit("❌ Missing one or more required environment variables. Check your .env file.")
# ----------------------------------------------------------------------
# Helper: Build auth header (Basic auth → ISE returns a JWT token)
# ----------------------------------------------------------------------
def _basic_auth_header() -> dict:
token = base64.b64encode(f"{ISE_USERNAME}:{ISE_PASSWORD}".encode()).decode()
return {"Authorization": f"Basic {token}"}
# ----------------------------------------------------------------------
# API wrappers with retry logic
# ----------------------------------------------------------------------
@retry(
reraise=True,
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((requests.ConnectionError, requests.Timeout)),
)
def _get(url: str, **kwargs) -> requests.Response:
"""GET with automatic retries on transient network errors."""
resp = requests.get(url, headers=_basic_auth_header(), verify=True, timeout=15, **kwargs)
resp.raise_for_status()
return resp
@retry(
reraise=True,
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=2, max=10),
retry=retry_if_exception_type((requests.ConnectionError, requests.Timeout)),
)
def _post(url: str, **kwargs) -> requests.Response:
"""POST with automatic retries."""
resp = requests.post(url, headers=_basic_auth_header(), verify=True, timeout=30, **kwargs)
resp.raise_for_status()
return resp
# ----------------------------------------------------------------------
# 1️⃣ Get current ISE version
# ----------------------------------------------------------------------
def get_ise_version() -> str:
url = f"{ISE_BASE_URL}/api/system/version"
resp = _get(url)
data = resp.json()
# Expected shape: {"response": {"version": "3.1.0.212", ...}}
version = data.get("response", {}).get("version")
if not version:
raise ValueError("Unexpected version response: %s" % data)
return version.strip()
# ----------------------------------------------------------------------
# 2️⃣ Upload patch bundle (multipart/form-data)
# ----------------------------------------------------------------------
def upload_patch(patch_path: Path) -> str:
"""
Returns the upload job ID (string) that can be used to monitor progress.
"""
url = f"{ISE_BASE_URL}/api/system/patch/upload"
files = {"file": (patch_path.name, patch_path.open("rb"), "application/octet-stream")}
resp = _post(url, files=files)
data = resp.json()
job_id = data.get("response", {}).get("jobId")
if not job_id:
raise ValueError("Patch upload did not return a jobId: %s" % data)
return job_id
# ----------------------------------------------------------------------
# 3️⃣ Install uploaded patch (triggers reboot if required)
# ----------------------------------------------------------------------
def install_patch(job_id: str) -> str:
"""
Returns the installation job ID.
"""
url = f"{ISE_BASE_URL}/api/system/patch/install"
payload = {"uploadJobId": job_id}
resp = _post(url, json=payload)
data = resp.json()
install_job_id = data.get("response", {}).get("jobId")
if not install_job_id:
raise ValueError("Patch install did not return a jobId: %s" % data)
return install_job_id
# ----------------------------------------------------------------------
# 4️⃣ Poll a job until completion (success/fail)
# ----------------------------------------------------------------------
def poll_job(job_id: str, timeout_seconds: int = 1800) -> bool:
"""
Returns True if job succeeded, False otherwise.
"""
url = f"{ISE_BASE_URL}/api/system/job/{job_id}"
start = time.time()
while time.time() - start < timeout_seconds:
resp = _get(url)
data = resp.json()
status = data.get("response", {}).get("status", "").lower()
if status in ("completed", "success"):
return True
if status in ("failed", "error", "canceled"):
print(f"❌ Job {job_id} ended with status: {status}")
return False
# still running – wait a bit
time.sleep(15)
print(f"⏰ Timeout waiting for job {job_id} to finish.")
return False
# ----------------------------------------------------------------------
# Main workflow
# ----------------------------------------------------------------------
def main() -> None:
print("🔎 Checking current ISE version...")
current_version = get_ise_version()
print(f"📦 Current version: {current_version}")
if current_version >= TARGET_VERSION:
print(f"✅ Already at or above target version {TARGET_VERSION}. No action needed.")
return
print(f"🚨 Version {current_version} is older than target {TARGET_VERSION}. Initiating patch...")
patch_path = Path(PATCH_FILE_PATH)
if not patch_path.is_file():
sys.exit(f"❌ Patch file not found: {patch_path}")
# Upload
print("📤 Uploading patch bundle...")
upload_job_id = upload_patch(patch_path)
print(f"📤 Upload job ID: {upload_job_id}")
if not poll_job(upload_job_id):
sys.exit("❌ Patch upload failed.")
# Install
print("💾 Installing patch...")
install_job_id = install_patch(upload_job_id)
print(f"💾 Install job ID: {install_job_id}")
if not poll_job(install_job_id):
sys.exit("❌ Patch installation failed.")
# Final version check
print("🔎 Verifying new version...")
new_version = get_ise_version()
print(f"📦 New version: {new_version}")
if new_version >= TARGET_VERSION:
print("🎉 Patch successfully applied!")
else:
print("⚠️ Version still below target – manual investigation required.")
if __name__ == "__main__":
try:
main()
except requests.HTTPError as e:
sys.exit(f"❌ HTTP error: {e.response.status_code} – {e.response.text}")
except Exception as exc: # pragma: no cover – safety net
sys.exit(f"❌ Unexpected error: {exc}")
isePatch.ts)/**
* isePatch.ts – Node.js/TypeScript automation for Cisco ISE emergency patch.
*
* Prerequisites:
* npm i axios dotenv
* (Optional) npm i -D typescript @types/node ts-node
*
* Environment variables (see .env example):
* ISE_BASE_URL – e.g. https://ise.example.com
* ISE_USERNAME – admin user
* ISE_PASSWORD – password or API token
* PATCH_FILE_PATH– absolute path to the patch bundle
* TARGET_VERSION – desired version string (e.g. "3.1.0.212")
*/
import axios from "axios";
import * as dotenv from "dotenv";
import * as fs from "fs";
import * as path from "path";
dotenv.config();
const ISE_BASE_URL: string = process.env.ISE_BASE_URL!.replace(/\/+$/, "");
const ISE_USERNAME: string = process.env.ISE_USERNAME!;
const ISE_PASSWORD: string = process.env.ISE_PASSWORD!;
const PATCH_FILE_PATH: string = process.env.PATCH_FILE_PATH!;
const TARGET_VERSION: string = process.env.TARGET_VERSION!;
if (![ISE_BASE_URL, ISE_USERNAME, ISE_PASSWORD, PATCH_FILE_PATH, TARGET_VERSION].every(Boolean)) {
console.error("❌ Missing one or more required environment variables. Check your .env file.");
process.exit(1);
}
// ----------------------------------------------------------------------
// Axios instance with basic auth + retry interceptor
// ----------------------------------------------------------------------
const api = axios.create({
baseURL: ISE_BASE_URL,
timeout: 15000,
headers: {
Authorization: `Basic ${Buffer.from(`${ISE_USERNAME}:${ISE_PASSWORD}`).toString("base64")`,
},
});
// Simple retry wrapper (exponential backoff)
async function requestWithRetry<T>(config: axios.AxiosRequestConfig): Promise<T> {
const maxAttempts = 5;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const resp = await api.request<T>(config);
return resp.data;
} catch (err: any) {
if (!axios.isAxiosError(err) || !err.response || err.response.status >= 500) {
// network or 5xx -> retry
if (attempt === maxAttempts) throw err;
const delay = Math.pow(2, attempt) * 1000; // 2s,4s,8s,16s,32s
await new Promise((r) => setTimeout(r, delay));
continue;
}
// 4xx -> not retryable
throw err;
}
}
throw new Error("Unreachable");
}
// ----------------------------------------------------------------------
// Helper functions
// ----------------------------------------------------------------------
async function getIseVersion(): Promise<string> {
const data = await requestWithRetry<{ response: { version: string } }>({
method: "GET",
url: "/api/system/version",
});
return data.response.version.trim();
}
async function uploadPatch(filePath: string): Promise<string> {
const form = new (await import("form-data")).default(); // dynamic import to avoid CJS/ESM issues
form.append("file", fs.createReadStream(filePath), {
filename: path.basename(filePath),
contentType: "application/octet-stream",
});
const { data } = await requestWithRetry<{ response: { jobId: string } }>({
method: "POST",
url: "/api/system/patch/upload",
data: form,
headers: { ...form.getHeaders() },
});
return data.response.jobId;
}
async function installPatch(uploadJobId: string): Promise<string> {
const { data } = await requestWithRetry<{ response: { jobId: string } }>({
method: "POST",
url: "/api/system/patch/install",
data: { uploadJobId: uploadJobId },
});
return data.response.jobId;
}
async function pollJob(jobId: string, timeoutSec = 1800): Promise<boolean> {
const start = Date.now();
while (Date.now() - start < timeoutSec * 1000) {
const { data } = await requestWithRetry<{ response: { status: string } }>({
method: "GET",
url: `/api/system/job/${jobId}`,
});
const status = data.response.status.toLowerCase();
if (status === "completed" || status === "success") return true;
if (status === "failed" || status === "error" || status === "canceled") {
console.error(`❌ Job ${jobId} ended with status: ${status}`);
return false;
}
await new Promise((r) => setTimeout(r, 15000)); // 15s
}
console.error(`⏰ Timeout waiting for job ${jobId}`);
return false;
}
// ----------------------------------------------------------------------
// Main routine
// ----------------------------------------------------------------------
(async () => {
try {
console.log("🔎 Checking current ISE version...");
const currentVersion = await getIseVersion();
console.log(`📦 Current version: ${currentVersion}`);
if (currentVersion >= TARGET_VERSION) {
console.log(`✅ Already at or above target version ${TARGET_VERSION}. No action needed.`);
return;
}
console.log(
`🚨 Version ${currentVersion} is older than target ${TARGET_VERSION}. Initiating patch...`
);
if (!fs.existsSync(PATCH_FILE_PATH)) {
throw new Error(`Patch file not found: ${PATCH_FILE_PATH}`);
}
// Upload
console.log("📤 Uploading patch bundle...");
const uploadJobId = await uploadPatch(PATCH_FILE_PATH);
console.log(`📤 Upload job ID: ${uploadJobId}`);
if (!(await pollJob(uploadJobId))) process.exit(1);
// Install
console.log("💾 Installing patch...");
const installJobId = await installPatch(uploadJobId);
console.log(`💾 Install job ID: ${installJobId}`);
if (!(await pollJob(installJobId))) process.exit(1);
// Verify
console.log("🔎 Verifying new version...");
const newVersion = await getIseVersion();
console.log(`📦 New version: ${newVersion}`);
if (newVersion >= TARGET_VERSION) {
console.log("🎉 Patch successfully applied!");
} else {
console.warn("⚠️ Version still below target – manual investigation required.");
}
} catch (err: any) {
if (axios.isAxiosError(err)) {
console.error(
`❌ HTTP error ${err.response?.status}: ${err.response?.data?.message || err.message}`
);
} else {
console.error(`❌ Unexpected error: ${err.message}`);
}
process.exit(1);
}
})();
Tip: To run the TypeScript version directly without a compile step:
npx ts-node isePatch.ts
(Make surets-nodeis installed as a dev dependency.)
<a name="step-4-configuration"></a>
Create a .env file in the project root (never commit this file to public repos).
Example:
# Cisco ISE connection
ISE_BASE_URL=https://ise01.example.com
ISE_USERNAME=admin_user
ISE_PASSWORD=SuperSecretPassword! # or an API token if your ISE uses token auth
# Patch details
PATCH_FILE_PATH=/opt/patches/ise-3.1.0.212-patch.zip
TARGET_VERSION=3.1.0.212
Optional – Token‑based auth (if your ISE is configured to accept OAuth2 client‑credentials):
/api/token) and replace the Authorization header with Bearer <token>.<a name="step-5-common-patterns"></a>
| Pattern | Description | Python snippet | JS/TS snippet |
|---|---|---|---|
| Retry with exponential backoff | Handles transient network glitches. | tenacity decorator (see requests_with_retry). | Custom requestWithRetry loop. |
| Token refresh | When using short‑lived OAuth tokens, refresh before expiry. | python\nif token_is_expired:\n token = fetch_new_token()\n | js\nif (isTokenExpired()) token = await fetchToken();\n |
| Chunked file upload | For very large patches (>100 MB) to avoid memory spikes. | Use requests-toolbelt.MultipartEncoder with monitor. | Use form-data with streams (already streams). |
| Idempotent patch check | Before uploading, query ISE for already‑applied patches to avoid re‑upload. | GET /api/system/patch/installed | Same endpoint in JS. |
| Structured logging | Easier parsing in SIEM tools. | import logging; logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s') | Use pino or winston with JSON format. |
| Secrets management | Avoid hard‑coding credentials. | Retrieve from AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault. | Same – use respective SDKs. |
<a name="step-6-troubleshooting"></a>
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized | Wrong credentials, missing base64 encoding, or API disabled. | Verify ISE_USERNAME/ISE_PASSWORD. Ensure the admin account has API access (Admin > System > Settings > API). |
403 Forbidden | Account lacks System Admin role or the endpoint is blocked by a policy. | Grant the System Admin role or create a custom admin with System/Update privileges. |
SSL: CERTIFICATE_VERIFY_FAILED | Self‑signed cert or internal CA not trusted. | Add the CA bundle to requests (verify="/path/to/ca.pem" ) or set NODE_EXTRA_CA_CERTS env var for Node. |
429 Too Many Requests | Rate‑limiting triggered by rapid retries. | Increase backoff (wait_exponential max) or add a Retry-After header handler. |
Patch upload succeeds but install job fails with error: patch not applicable | Patch version mismatch (e.g., trying to apply a 2.9 patch to a 3.1 system). | Double‑check the patch bundle matches the target ISE major/minor version. |
Job polls forever (status stays running) | The patch installation is waiting for a manual reboot or user confirmation. | Check ISE GUI for prompts; some patches require a manual “Apply and Reboot” step. In that case, trigger the reboot via /api/system/reboot after install. |
ConnectionError / Timeout | Network firewall blocking port 443 or DNS resolution failure. | Verify connectivity (curl -k https://ise.example.com/api/system/version). Open firewall rules or correct /etc/resolv.conf. |
<a name="step-7-production-checklist"></a>
| ✅ Item | Why it matters |
|---|---|
Use a secrets manager (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault) instead of plain .env in production. | |
Least‑privilege API account – create a dedicated service account with only System/Update and System/Read rights. | |
| Enable mutual TLS (client certificates) if your ISE deployment supports it; eliminates password‑based auth risks. | |
| Log all API calls (request/response metadata) to a centralized SIEM for audit trails. | |
Run a dry‑run first – hit /api/system/version and /api/system/patch/available to confirm the patch is needed before uploading. | |
Schedule patch deployment during a maintenance window; use the /api/system/reboot endpoint only after confirming install success. | |
| Validate checksum of the downloaded patch file (SHA‑256) against the value published in the Cisco PSIRT advisory. | |
| Test in a non‑production ISE node (e.g., a lab or standby) before pushing to the primary admin node. | |
Monitor post‑patch health – after reboot, poll /api/system/health and critical RADIUS/TACACS+ endpoints for 15‑30 min. | |
| Document the runbook – include the exact commands, expected output, and rollback steps (re‑apply previous snapshot if available). | |
| Rotate credentials – change the service account password/token after the patch window expires. | |
Backup configuration – execute /api/system/config/export before starting, store the backup off‑box. |
Copy the scripts, adjust the .env values, and run them in your automation pipeline (CI/CD, Ansible, Jenkins, etc.). They will safely verify the current Cisco ISE version, upload the emergency patch, trigger installation, and confirm success—all with proper error handling, retries, and logging.
Stay secure, and keep those patches rolling!
Author: ICARAX Engineering Team
Date: 2025‑11‑02
Version: 1.0
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
