

Implementation guide for developers – how to verify the fix and safely interact with the Cisco ISE REST API.
<a name="step-1-prerequisites"></a>
| Item | Minimum version / notes |
|---|---|
| Cisco ISE | Any release that includes the patch for the command‑execution CVE (e.g., ISE 3.1 Patch 2 or later). You must have admin or API‑admin privileges. |
| API Access | REST API enabled on the ISE Administration node (Administration → System → Settings → REST API). |
| Python | 3.8+ (recommended 3.11). |
| Node.js | 14.x LTS or newer (for JS/TS). |
| Package Managers | pip (Python) and npm or yarn (Node). |
| IDE / Editor | VS Code, PyCharm, WebStorm, etc. |
| Network | Ability to reach the ISE HTTPS endpoint (typically https://<ise-hostname>:9060/ers/). |
Security note – Never hard‑code credentials in source control. Use environment variables or a secret manager (see Step 4).
<a name="step-2-installation-and-setup"></a>
# 1️⃣ Create a virtual environment (optional but 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
# 1️⃣ Initialise a new Node project (skip if you already have one)
npm init -y # or: yarn init -y
# 2️⃣ Install dependencies
npm install axios dotenv # for plain JS
# If you prefer TypeScript:
npm install --save-dev typescript @types/node ts-node
npx tsc --init # creates tsconfig.json
Tip – Add
"type": "module"topackage.jsonif you want to use ES‑modules (import … from …). The examples below use CommonJS (require) for maximum compatibility.
<a name="step-3-basic-implementation"></a>
The goal of the sample code is to authenticate to Cisco ISE, retrieve the system version, and compare it against the known patched version.
If the version is equal to or newer than the patched release, the script reports “Patch applied”. Otherwise it warns that the system may still be vulnerable.
Replace
<ISE_HOST>,<ISE_USER>,<ISE_PASSWORD>with your own values (see Step 4 for env‑file usage).
check_ise_patch.py)#!/usr/bin/env python3
"""
check_ise_patch.py
------------------
Verifies whether the Cisco ISE instance is running a version that includes
the critical command‑execution patch.
Requires:
- requests
- python-dotenv
"""
import os
import sys
import logging
from typing import Tuple
import requests
from dotenv import load_dotenv
# ----------------------------------------------------------------------
# Configuration (loaded from .env)
# ----------------------------------------------------------------------
load_dotenv() # pulls variables from .env into os.environ
ISE_HOST = os.getenv("ISE_HOST") # e.g., ise.example.com
ISE_USER = os.getenv("ISE_USER")
ISE_PASSWORD = os.getenv("ISE_PASSWORD")
ISE_PORT = int(os.getenv("ISE_PORT", "9060")) # default REST API port
USE_SSL_VERIFY = os.getenv("ISE_SSL_VERIFY", "true").lower() == "true"
# Minimum version that contains the fix (adjust per advisory)
# Format: major.minor.patch (as returned by ISE /api/system/version)
PATCHED_VERSION: Tuple[int, int, int] = (3, 1, 2) # Example: ISE 3.1 Patch 2
# ----------------------------------------------------------------------
# Logging setup
# ----------------------------------------------------------------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s - %(message)s",
)
log = logging.getLogger(__name__)
# ----------------------------------------------------------------------
# Helper functions
# ----------------------------------------------------------------------
def get_auth_token() -> str:
"""
Obtain an ERS API token using basic auth.
Returns the token string.
"""
url = f"https://{ISE_HOST}:{ISE_PORT}/ers/config/systeminfo"
# The /systeminfo endpoint does NOT require a token, but we use it to
# trigger auth and capture the returned token header.
resp = requests.get(
url,
auth=(ISE_USER, ISE_PASSWORD),
verify=USE_SSL_VERIFY,
timeout=10,
)
resp.raise_for_status()
# ISE returns the token in the header 'X-ERSAPI-Authenticated-User'
# but the actual token is needed for subsequent calls via the
# 'ERS-API' header (or we can reuse basic auth). For simplicity we
# continue with basic auth; the token extraction is shown for completeness.
token = resp.headers.get("X-ERSAPI-Authenticated-User")
if token:
log.debug("Authenticated as %s", token)
return token # not strictly needed for the demo
def fetch_system_version() -> Tuple[int, int, int]:
"""
Calls the ISE REST API to retrieve the system version.
Returns a tuple (major, minor, patch).
"""
url = f"https://{ISE_HOST}:{ISE_PORT}/api/system/version"
resp = requests.get(
url,
auth=(ISE_USER, ISE_PASSWORD),
verify=USE_SSL_VERIFY,
timeout=10,
)
resp.raise_for_status()
data = resp.json()
# Expected JSON: {"version": "3.1.2.0", "buildDate": "2024-04-01", ...}
version_str = data.get("version", "0.0.0")
# Strip any extra build metadata (e.g., "3.1.2.0" -> "3.1.2")
core_version = ".".join(version_str.split(".")[:3])
try:
major, minor, patch = map(int, core_version.split("."))
except ValueError as exc:
log.error("Unable to parse version string '%s': %s", version_str, exc)
raise
log.info("ISE reports version %s.%s.%s", major, minor, patch)
return major, minor, patch
def is_patched(current: Tuple[int, int, int]) -> bool:
"""
Simple semantic‑version comparison: returns True if current >= PATCHED_VERSION.
"""
return current >= PATCHED_VERSION
# ----------------------------------------------------------------------
# Main execution
# ----------------------------------------------------------------------
def main() -> int:
try:
# Optional: obtain a token (demonstrates auth flow)
get_auth_token()
version = fetch_system_version()
if is_patched(version):
print(f"✅ ISE version {'.'.join(map(str, version)}} is **patched** (≥ {'.'.join(map(str, PATCHED_VERSION))}).")
return 0
else:
print(
f"⚠️ ISE version {'.'.join(map(str, version)}} is **older** than the patched version "
f"{'.'.join(map(str, PATCHED_VERSION))}. Please upgrade immediately."
)
return 1
except requests.exceptions.RequestException as exc:
log.error("HTTP request failed: %s", exc)
return 2
except Exception as exc: # pragma: no cover – defensive
log.exception("Unexpected error: %s", exc)
return 3
if __name__ == "__main__":
sys.exit(main())
How to run
# 1️⃣ Create .env (see Step 4)
cp .env.example .env # then edit .env with your values
# 2️⃣ Execute
python check_ise_patch.py
checkIsePatch.js)/**
* checkIsePatch.js
* ----------------
* Node.js script that validates whether a Cisco ISE instance is running a
* version that includes the critical command‑execution patch.
*
* Requires:
* - axios
* - dotenv
*/
require('dotenv').config(); // loads .env into process.env
const axios = require('axios');
const { log, error } = require('console');
// ----------------------------------------------------------------------
// Configuration from environment
// ----------------------------------------------------------------------
const ISE_HOST = process.env.ISE_HOST;
const ISE_USER = process.env.ISE_USER;
const ISE_PASSWORD = process.env.ISE_PASSWORD;
const ISE_PORT = parseInt(process.env.ISE_PORT) || 9060;
const SSL_VERIFY = process.env.ISE_SSL_VERIFY !== 'false';
// Minimum version that contains the fix (adjust per advisory)
const PATCHED_VERSION = [3, 1, 2]; // Example: ISE 3.1 Patch 2
// ----------------------------------------------------------------------
// Helper: semantic version compare (returns true if a >= b)
// ----------------------------------------------------------------------
function versionAtLeast(current, required) {
for (let i = 0; i < 3; i++) {
if (current[i] > required[i]) return true;
if (current[i] < required[i]) return false;
}
return true; // equal
}
// ----------------------------------------------------------------------
// Main
// ----------------------------------------------------------------------
(async () => {
try {
const baseURL = `https://${ISE_HOST}:${ISE_PORT}`;
// Optional: verify connectivity via /systeminfo (no token needed)
await axios.get(`${baseURL}/ers/config/systeminfo`, {
auth: { username: ISE_USER, password: ISE_PASSWORD },
validateStatus: status => status < 500, // treat 4xx as resolvable
httpsAgent: new (require('https')).Agent({ rejectUnauthorized: SSL_VERIFY }),
timeout: 10000,
});
// Fetch version
const { data } = await axios.get(`${baseURL}/api/system/version`, {
auth: { username: ISE_USER, password: ISE_PASSWORD },
validateStatus: status => status < 500,
httpsAgent: new (require('https')).Agent({ rejectUnauthorized: SSL_VERIFY }),
timeout: 10000,
});
// Expected shape: { version: "3.1.2.0", ... }
const rawVersion = data.version || '0.0.0';
const [major, minor, patch] = rawVersion
.split('.')
.slice(0, 3)
.map(v => parseInt(v, 10));
log.info(`ISE reports version ${major}.${minor}.${patch}`);
const current = [major, minor, patch];
if (versionAtLeast(current, PATCHED_VERSION)) {
console.log(
`✅ ISE version ${major}.${minor}.${patch} is **patched** (≥ ${PATCHED_VERSION.join('.')}).`
);
process.exit(0);
} else {
console.warn(
`⚠️ ISE version ${major}.${minor}.${patch} is **older** than the patched version ${PATCHED_VERSION.join(
'.'
)}. Please upgrade immediately.`
);
process.exit(1);
}
} catch (err) {
if (err.isAxiosError) {
error(`HTTP error: ${err.message}`);
if (err.response) {
error(`Status: ${err.response.status}`);
error(`Data: ${JSON.stringify(err.response.data)}`);
}
} else {
error(`Unexpected error: ${err}`);
}
process.exit(2);
}
})();
How to run
# 1️⃣ Create .env (see Step 4)
cp .env.example .env # edit with your values
# 2️⃣ Execute
node checkIsePatch.js
(If you prefer TypeScript, rename the file to .ts, run ts-node checkIsePatch.ts, and ensure tsconfig.json targets ES2020 or later.)
<a name="step-4-configuration"></a>
Create a .env file in the project root (never commit this file).
A sample template (.env.example) is shown below:
# .env.example – copy to .env and fill in your values
ISE_HOST=ise.example.com # hostname or IP of the ISE admin node
ISE_USER=api_admin # a user with ERS/API admin rights
ISE_PASSWORD=SuperSecretPassword! # password or API token (if using token auth)
ISE_PORT=9060 # default REST API port; change if you customized
ISE_SSL_VERIFY=true # set to false only for self‑signed certs (dev only)
If your ISE is configured to accept Bearer tokens instead of basic auth, replace the auth section in the code:
# Python
headers = {"Authorization": f"Bearer {process.env.ISE_TOKEN}"}
// JavaScript
headers: { Authorization: `Bearer ${process.env.ISE_TOKEN}` }
You would then obtain a token via the /ers/api/v1/auth/token endpoint (see Cisco ISE API guide) and store it in ISE_TOKEN.
<a name="step-5-common-patterns"></a>
| Pattern | Description | Code snippet |
|---|---|---|
Reusable axios/requests session | Reduces TLS handshake overhead and lets you set default headers/auth. | python\nsession = requests.Session()\nsession.auth = (ISE_USER, ISE_PASSWORD)\nsession.verify = USE_SSL_VERIFY\nresp = session.get(url)\n |
| Pagination handling | Many ERS endpoints return paginated results (SearchResult with page/size). | javascript\nlet page = 0;\nlet hasMore = true;\nwhile (hasMore) {\n const res = await axios.get(`${baseURL}/ers/config/networkdevice`, {\n params: { size: 100, page: page++ }\n });\n // process res.data.SearchResult.resources\n hasMore = res.data.SearchResult.hasNextPage;\n}\n |
| Retry with exponential backoff | Mitigates transient network glitches. | python\nfrom urllib3.util.retry import Retry\nfrom requests.adapters import HTTPAdapter\n\nretries = Retry(total=3, backoff_factor=1, status_forcelist=[502, 503, 504])\nsession.mount('https://', HTTPAdapter(max_retries=retries))\n |
| Circuit‑breaker pattern | Prevents hammering an unhealthy ISE node. | Use libraries like opossum (Node) or pybreaker (Python). |
| Structured logging | Easier ingestion by SIEM / log‑aggregation tools. | python\nimport logging, json\nclass JsonFormatter(logging.Formatter):\n def format(self, record):\n return json.dumps({\n 'time': self.formatTime(record),\n 'level': record.levelname,\n 'msg': record.getMessage(),\n 'name': record.name\n })\nhandler = logging.StreamHandler()\nhandler.setFormatter(JsonFormatter())\nlogging.basicConfig(level=logging.INFO, handlers=[handler])\n |
<a name="step-6-troubleshooting"></a>
| Symptom | Likely cause | Fix |
|---|---|---|
401 Unauthorized | Wrong credentials, API disabled, or account locked. | Verify ISE_USER/ISE_PASSWORD; ensure the user has ERS Admin or API Admin role; confirm REST API is enabled (Administration → System → Settings → REST API). |
SSL: CERTIFICATE_VERIFY_FAILED | Self‑signed or internal CA cert not trusted. | Set ISE_SSL_VERIFY=false only for testing; better: add the ISE CA to your system trust store or provide a custom CA bundle via verify=/path/to/ca.pem. |
ECONNREFUSED / timeout | Hostname/IP wrong, port blocked, or ISE service down. | Ping the host, try telnet <ISE_HOST> 9060; check firewalls/ACLs; ensure you’re reaching the admin node (not a PSN). |
404 Not Found on /api/system/version | Using an older ISE release that doesn’t expose this endpoint. | Fall back to /ers/config/systeminfo (returns version in XML) or consult the version via CLI (show version). |
| Empty or malformed JSON response | Intermediate proxy altering content, or authentication redirecting to login page. | Disable any HTTP proxy (NO_PROXY env var); ensure you’re hitting the correct base URL (:9060/ers/). |
Max retries exceeded after many attempts | Rate‑limiting or temporary lockout due to failed logins. | Slow down requests; implement backoff; verify no brute‑force attempts are occurring. |
Debug tip – Enable verbose logging:
# Python
export PYTHONWARNINGS="default"
python -Wd check_ise_patch.py # shows warnings, including insecure request warnings
# Node
DEBUG=axios node checkIsePatch.js
<a name="step-7-production-checklist"></a>
| ✅ Item | Why it matters |
|---|---|
| Use HTTPS with valid certificates | Prevents MITM; ensures confidentiality of credentials and data. |
| Store secrets in a vault (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, etc.) | Avoids accidental exposure in source control or logs. |
| Least‑privilege API account | Create a dedicated service account with only the needed ERS permissions (e.g., ers:read:systeminfo). |
| Enable request/response logging (but mask passwords) | Helps with audit trails and post‑incident forensics. |
| Implement retry & circuit‑breaker | Increases resilience against transient network issues. |
Validate TLS certificates (verify=true in production) | Stops connections to spoofed endpoints. |
| Regularly rotate credentials | Limits the window of compromise if a secret leaks. |
| Monitor version compliance | Schedule the version‑check script (cron, CloudWatch, etc.) and alert if version < patched. |
| Patch management process | Subscribe to Cisco security advisories; test patches in a staging window before prod rollout. |
| Network segmentation | Restrict API access to only trusted management subnets or jump hosts. |
| Disable unused services (e.g., SSH, Telnet) on ISE nodes | Reduces attack surface. |
| Backup configuration before applying patches | Enables quick rollback if needed. |
| Document runbooks for patch verification and incident response. | Ensures consistent, repeatable actions by the team. |
You now have:
check_ise_patch.py) that authenticates to Cisco ISE, reads the version, and reports patch status.checkIsePatch.js) that does the same.Feel free to adapt the snippets to your CI/CD pipelines, monitoring tools, or internal automation frameworks. Stay safe, keep your ISE up‑to‑date, and remember: the best defense is a verified patch.
Happy coding, and stay secure! 🚀
Source: Security Week AI
Follow ICARAX for more AI insights and tutorials.
