Test your knowledge on this topic in the ENAUTO Exam Trainer — 186 questions across 5 interactive modes.
Security Automation Across Controllers – ENAUTO v2.0
Exam Relevance
Topic 3.0 Controller-Based Network Automation (30%) — Security automation spans all four controllers.
This guide fills the gaps left by the individual Deep Dives by covering Catalyst Center Command Runner for compliance, Meraki firewall/group policy APIs, and a cross-controller segmentation comparison.
Exam Topics Covered:
3.2 – Python to manage and monitor configurations
3.5 – Security automation (policy enforcement, compliance monitoring, segmentation)
3.6 – Troubleshoot REST API automation solutions
4.4 – Monitor network health (compliance as health dimension)
Security automation in the ENAUTO context means using controller APIs to enforce policies, monitor compliance, and segment networks — without manual CLI changes.
POST /dna/intent/api/v1/business/sda/virtual-network
Application policy
Catalyst Center
POST /dna/intent/api/v1/app-policy
2 – Catalyst Center: Command Runner for Compliance
Exam Topic: 3.2, 3.5
Command Runner lets you execute read-only CLI commands on managed devices through the Catalyst Center API. This is the primary mechanism for automated compliance checks — run show commands across your fleet, parse output, and flag deviations.
Command Runner Is a 3-Step Async Process
POST the CLI command → receive a taskId
Poll the task → receive a fileId in the task progress field
GET the file → receive the actual CLI output
Forgetting step 2 or 3 is a very common exam trap.
# --- Step 2: Poll task until completion ---task_url = f"{BASE_URL}/dna/intent/api/v1/task/{task_id}"for attempt in range(10): task_response = requests.get(task_url, headers=headers, verify=False) task_data = task_response.json()["response"] if task_data.get("isError"): print(f"Task failed: {task_data.get('failureReason')}") break if "fileId" in task_data.get("progress", ""): # progress contains a JSON string with fileId progress = json.loads(task_data["progress"]) file_id = progress["fileId"] print(f"File ID: {file_id}") break print(f"Attempt {attempt + 1}: Task still in progress...") time.sleep(2)
Step 3 – Retrieve the CLI Output
# --- Step 3: Get the actual CLI output ---file_url = f"{BASE_URL}/dna/intent/api/v1/file/{file_id}"file_response = requests.get(file_url, headers=headers, verify=False)command_outputs = file_response.json()for device_output in command_outputs: device_id = device_output["deviceUuid"] output = device_output["commandResponses"]["SUCCESS"].get("show access-lists", "") print(f"\n--- Device: {device_id} ---") print(output)
Full Example – AAA Compliance Check
import requestsimport timeimport jsonBASE_URL = "https://catalyst-center.example.com"def get_token(username, password): """Authenticate and return X-Auth-Token.""" url = f"{BASE_URL}/dna/system/api/v1/auth/token" resp = requests.post(url, auth=(username, password), verify=False) resp.raise_for_status() return resp.json()["Token"]def run_command(headers, device_uuids, commands): """Run CLI commands via Command Runner and return parsed output.""" # Step 1: Submit command url = f"{BASE_URL}/dna/intent/api/v1/network-device-poller/cli/read-request" payload = {"commands": commands, "deviceUuids": device_uuids} resp = requests.post(url, headers=headers, json=payload, verify=False) task_id = resp.json()["response"]["taskId"] # Step 2: Poll task task_url = f"{BASE_URL}/dna/intent/api/v1/task/{task_id}" for _ in range(15): task_resp = requests.get(task_url, headers=headers, verify=False) task_data = task_resp.json()["response"] if task_data.get("isError"): raise Exception(f"Task failed: {task_data.get('failureReason')}") progress = task_data.get("progress", "") if "fileId" in progress: file_id = json.loads(progress)["fileId"] break time.sleep(2) else: raise TimeoutError("Task did not complete within timeout") # Step 3: Get output file_url = f"{BASE_URL}/dna/intent/api/v1/file/{file_id}" return requests.get(file_url, headers=headers, verify=False).json()# --- Main compliance check ---token = get_token("admin", "C1sco12345")headers = {"X-Auth-Token": token, "Content-Type": "application/json"}# Get all devicesdevices_url = f"{BASE_URL}/dna/intent/api/v1/network-device"devices = requests.get(devices_url, headers=headers, verify=False).json()["response"]device_uuids = [d["id"] for d in devices if d["family"] == "Switches and Hubs"]# Run AAA compliance checkoutputs = run_command(headers, device_uuids, ["show run | section aaa"])# Parse and flag non-compliant devicesREQUIRED_AAA_LINES = [ "aaa new-model", "aaa authentication login default group tacacs+", "aaa authorization exec default group tacacs+"]non_compliant = []for device_output in outputs: device_id = device_output["deviceUuid"] aaa_config = device_output["commandResponses"]["SUCCESS"].get( "show run | section aaa", "" ) missing = [line for line in REQUIRED_AAA_LINES if line not in aaa_config] if missing: non_compliant.append({ "deviceUuid": device_id, "missing_config": missing })print(f"\n=== Compliance Report ===")print(f"Total devices checked: {len(outputs)}")print(f"Non-compliant devices: {len(non_compliant)}")for nc in non_compliant: print(f" Device {nc['deviceUuid']}: missing {nc['missing_config']}")
Command Runner Key Points
Only read-only commands are allowed (show, display) — no configuration changes
Device UUIDs come from GET /dna/intent/api/v1/network-device
The progress field in the task response is a JSON string that must be parsed with json.loads()
Maximum 5 commands per request, maximum 20 devices per request
3 – Catalyst Center: Network Access Control
Exam Topic: 3.5
Catalyst Center with SDA (Software-Defined Access) provides network segmentation through virtual networks, IP pools, and fabric sites. These are configured through the /dna/intent/api/v1/business/sda/ endpoints.
Catalyst Center also supports application-aware policies via /dna/intent/api/v1/app-policy. These tie QoS and security policies to application categories (e.g., block peer-to-peer traffic, prioritize voice).
Like all Catalyst Center write operations, SDA endpoint POSTs return a taskId. You must poll the task to confirm success. Never assume a 200 on the initial POST means the resource is created.
4 – Meraki: MX Firewall Rules
Exam Topic: 3.2, 3.5
Meraki MX appliances expose L3 firewall rules through a simple REST API. The critical concept: PUT replaces the entire rule list — it does not append.
Get Current L3 Firewall Rules
import requestsAPI_KEY = "your_meraki_api_key"BASE_URL = "https://api.meraki.com/api/v1"NETWORK_ID = "L_123456789012345678"headers = { "X-Cisco-Meraki-API-Key": API_KEY, "Content-Type": "application/json"}# --- Get current L3 firewall rules ---fw_url = f"{BASE_URL}/networks/{NETWORK_ID}/appliance/firewall/l3FirewallRules"response = requests.get(fw_url, headers=headers)rules = response.json()["rules"]for i, rule in enumerate(rules): print(f"Rule {i}: {rule['policy']} {rule['protocol']} " f"src={rule['srcCidr']} dst={rule['destCidr']} port={rule['destPort']}")
Add a Deny Rule (Safely)
PUT Replaces the Entire Rule List
If you PUT only your new rule, all existing rules will be deleted. You must:
GET current rules
Insert your new rule into the list
PUT the complete updated list
The last rule is always the implicit “Default rule” (allow all) — do not include it in your PUT payload; Meraki re-adds it automatically.
import requestsimport timeAPI_KEY = "your_meraki_api_key"BASE_URL = "https://api.meraki.com/api/v1"NETWORK_ID = "L_123456789012345678"headers = { "X-Cisco-Meraki-API-Key": API_KEY, "Content-Type": "application/json"}# Step 1: GET current rulesfw_url = f"{BASE_URL}/networks/{NETWORK_ID}/appliance/firewall/l3FirewallRules"response = requests.get(fw_url, headers=headers)current_rules = response.json()["rules"]# Step 2: Remove the default "allow all" rule (Meraki re-adds it automatically)custom_rules = [r for r in current_rules if r.get("comment") != "Default rule"]# Step 3: Insert new deny rule at the top (rules are evaluated top-down)new_rule = { "comment": "Block torrent traffic", "policy": "deny", "protocol": "tcp", "srcCidr": "Any", "srcPort": "Any", "destCidr": "Any", "destPort": "6881-6889"}custom_rules.insert(0, new_rule)# Step 4: PUT the complete rule listput_response = requests.put( fw_url, headers=headers, json={"rules": custom_rules})if put_response.status_code == 200: print("Firewall rules updated successfully") print(f"Total rules: {len(put_response.json()['rules'])}")else: print(f"Error: {put_response.status_code} — {put_response.text}")
Using the Meraki SDK
import merakidashboard = meraki.DashboardAPI(API_KEY)# Get rulesrules = dashboard.appliance.getNetworkApplianceFirewallL3FirewallRules(NETWORK_ID)# Add a rule (still must include all existing rules)custom_rules = [r for r in rules["rules"] if r.get("comment") != "Default rule"]custom_rules.insert(0, { "comment": "Block torrent traffic", "policy": "deny", "protocol": "tcp", "srcCidr": "Any", "srcPort": "Any", "destCidr": "Any", "destPort": "6881-6889"})dashboard.appliance.updateNetworkApplianceFirewallL3FirewallRules( NETWORK_ID, rules=custom_rules)
Rate Limiting Reminder
Meraki enforces 5 API calls per second per organization. The SDK handles 429 retries automatically. With requests, implement exponential backoff or add time.sleep(0.2) between calls.
5 – Meraki: Group Policies
Exam Topic: 3.5
Group policies in Meraki apply a set of restrictions (bandwidth limits, firewall rules, content filtering, scheduling) to groups of clients. Clients are assigned to a group policy either manually or via RADIUS (ISE integration).
Meraki Adaptive Policy is the Meraki equivalent of Cisco TrustSec SGTs. It allows micro-segmentation within Meraki networks using security groups and ACLs that define allowed traffic between groups.
# --- Get adaptive policy groups (equivalent of SGTs) ---groups_url = f"{BASE_URL}/organizations/{ORG_ID}/adaptivePolicy/groups"response = requests.get(groups_url, headers=headers)groups = response.json()for group in groups: print(f"Group: {group['name']} (SGT: {group.get('sgt', 'N/A')}) " f"ID: {group['groupId']}")
Get Adaptive Policy Settings (the Matrix)
# --- Get the adaptive policy matrix (bindings between groups) ---policies_url = f"{BASE_URL}/organizations/{ORG_ID}/adaptivePolicy/policies"response = requests.get(policies_url, headers=headers)policies = response.json()for policy in policies: src = policy["sourceGroup"]["name"] dst = policy["destinationGroup"]["name"] acls = [a["name"] for a in policy.get("acls", [])] print(f"{src} -> {dst}: ACLs={acls}")
ISE Integration with Meraki Adaptive Policy
When ISE is integrated with Meraki, SGT values assigned by ISE are mapped to Meraki adaptive policy groups. The SGT number in Meraki corresponds directly to the TrustSec SGT value in ISE, enabling consistent policy across wired (ISE-managed) and wireless (Meraki-managed) networks.
7 – Cross-Controller Segmentation Comparison
Exam Topic: 3.5
This is one of the most important comparison tables for the exam. Segmentation is implemented differently across all four controllers:
Segmentation Comparison Matrix
Aspect
Catalyst Center (SDA)
ISE (TrustSec)
Meraki (Adaptive Policy)
SD-WAN (VPN Segmentation)
Segmentation model
Virtual Networks + SGTs
SGTs + SGACLs
Adaptive Policy Groups + ACLs
VPN IDs (0-65530)
Identity source
ISE integration required
Local DB + AD/LDAP + certificates
Dashboard manual or RADIUS
Not identity-based
Enforcement point
Fabric edge switches
Any TrustSec-capable switch
MR access points, MS switches, MX firewalls
vEdge / cEdge routers
API for automation
/dna/intent/api/v1/business/sda/
/ers/config/sgt, /ers/config/sgacl
/organizations/{orgId}/adaptivePolicy/
/template/policy/vsmart
Granularity
Micro-segmentation (per-user/device)
Micro-segmentation (per-user/device)
Macro + group-level
VPN-level (coarse)
Auth method
X-Auth-Token (token exchange)
Basic Auth (every request)
X-Cisco-Meraki-API-Key (every request)
JSESSIONID cookie
Write behavior
Asynchronous (taskId polling)
Synchronous
Synchronous
Asynchronous
Scale
Campus/branch fabric
Enterprise-wide
Cloud-managed sites
WAN overlay
Dependency
Requires ISE for identity
Standalone or with Catalyst Center
Standalone (optional ISE)
Standalone vManage
When to Use Which
Decision Tree for Segmentation:
Q: Is it a campus/branch fabric?
→ YES: Catalyst Center SDA + ISE TrustSec
Q: Is it a cloud-managed Meraki site?
→ YES: Meraki Adaptive Policy (with optional ISE for identity)
Q: Is it a WAN overlay?
→ YES: SD-WAN VPN segmentation
Q: Do you need enterprise-wide SGT policy regardless of fabric type?
→ YES: ISE TrustSec (the central policy engine)
8 – Compliance Monitoring Patterns
Exam Topic: 3.5, 4.4
Pattern 1 – Scheduled Compliance Checks with Command Runner
Run Command Runner on a cron schedule to verify device configurations match expected baselines.
# Pseudocode for a scheduled compliance workflow## 1. Get all managed devices from Catalyst Center# 2. For each batch of 20 devices:# a. Run Command Runner: "show run | section aaa"# b. Parse output against compliance baseline# c. Flag non-compliant devices# 3. Generate compliance report# 4. (Optional) Open ServiceNow ticket for non-compliant devicesimport scheduleimport timedef compliance_check(): token = get_token("admin", "C1sco12345") headers = {"X-Auth-Token": token, "Content-Type": "application/json"} devices = get_all_devices(headers) # Process in batches of 20 (Command Runner limit) for batch in chunks(devices, 20): uuids = [d["id"] for d in batch] outputs = run_command(headers, uuids, ["show run | section aaa"]) check_compliance(outputs)# Run every day at 2:00 AMschedule.every().day.at("02:00").do(compliance_check)while True: schedule.run_pending() time.sleep(60)
Pattern 2 – Webhook-Triggered Re-Validation
When a configuration change event fires, re-check the changed device immediately.
from flask import Flask, requestapp = Flask(__name__)@app.route("/webhook/config-change", methods=["POST"])def config_change_handler(): event = request.json device_id = event.get("deviceId") # Re-run compliance check on the changed device token = get_token("admin", "C1sco12345") headers = {"X-Auth-Token": token, "Content-Type": "application/json"} outputs = run_command(headers, [device_id], ["show run | section aaa"]) violations = check_compliance(outputs) if violations: # Trigger remediation or alert send_alert(f"Device {device_id} is non-compliant after config change") return {"status": "processed"}, 200
Pattern 3 – ISE ANC (Adaptive Network Control) for Quarantine
ISE ANC lets you quarantine, un-quarantine, or shutdown endpoints via API.
ISE ERS API Must Be Enabled Separately
The ERS (External RESTful Services) API is disabled by default on ISE. Enable it at:
Administration → Settings → API Settings → ERS (Read/Write)
Also ensure your API user has the ERS Admin role.
import requestsISE_BASE = "https://ise.example.com:9060"ISE_USER = "ersadmin"ISE_PASS = "C1sco12345"headers = { "Content-Type": "application/json", "Accept": "application/json" # ISE defaults to XML — always set this}# --- Quarantine an endpoint ---anc_url = f"{ISE_BASE}/ers/config/ancendpoint/apply"payload = { "OperationAdditionalData": { "additionalData": [ {"name": "macAddress", "value": "AA:BB:CC:DD:EE:FF"}, {"name": "policyName", "value": "ANC_Quarantine"} ] }}response = requests.put( anc_url, auth=(ISE_USER, ISE_PASS), headers=headers, json=payload, verify=False)if response.status_code == 204: print("Endpoint quarantined successfully")elif response.status_code == 403: print("ERROR: ERS API not enabled or user lacks ERS Admin role")else: print(f"Unexpected status: {response.status_code} — {response.text}")
Many exam questions test whether you know the full flow:
POST /cli/read-request → taskId
GET /task/{taskId} → poll until progress contains fileId
GET /file/{fileId} → actual CLI output
If your script gets the taskId but no CLI output, you forgot steps 2 and/or 3.
Meraki Firewall Rules Are REPLACE, Not APPEND
PUT /l3FirewallRules replaces the entire rule list. If you PUT a single rule, all other rules are deleted. Always GET first, merge, then PUT.
ISE ERS Must Be Enabled Separately
The ISE ERS API (/ers/config/...) is disabled by default. It is a separate system from the ISE Open API (/api/...). Enable ERS at Administration → Settings → API Settings.
ISE Defaults to XML Responses
If you forget Accept: application/json in your ISE ERS requests, you get XML back. This breaks response.json() with a JSONDecodeError.
Always Backup Before Modifying Firewall Rules
Before any PUT to Meraki firewall rules or ISE SGACLs, save the current state:
import json# Backup current rulescurrent_rules = requests.get(fw_url, headers=headers).json()with open("firewall_backup.json", "w") as f: json.dump(current_rules, f, indent=2)
SD-WAN Writes Require XSRF Token
SD-WAN (vManage) requires a XSRF token for any write operation in addition to the JSESSIONID session cookie:
# Get XSRF token after authenticationtoken_resp = requests.get( f"{VMANAGE_URL}/dataservice/client/token", cookies=session_cookies, verify=False)xsrf_token = token_resp.textheaders["X-XSRF-TOKEN"] = xsrf_token
10 – Exam-Style Scenarios
Scenario 1 – Meraki Firewall Rules Disappear
A network engineer runs the following script to add a deny rule to a Meraki MX firewall. After execution, the MX has only one rule (the new deny rule) and the default allow rule — all previous rules are gone. What went wrong?
The script PUTs only the new rule without including existing rules. Meraki’s PUT /l3FirewallRulesreplaces the entire rule list. The fix is to GET current rules first, append the new rule, then PUT the complete list. The default “allow all” rule is re-added automatically by Meraki.
Scenario 2 – Command Runner Returns taskId But No Output
A Python script uses Catalyst Center Command Runner to run show access-lists. The POST returns successfully with a taskId, but the script never gets the CLI output. The script only contains:
PollGET /task/{taskId} until progress contains a fileId (missing)
GET/file/{fileId} to retrieve the actual CLI output (missing)
The script stops after step 1 and never retrieves the output.
Scenario 3 – ISE ANC Quarantine Fails with 403
A script attempts to quarantine an endpoint via ISE ANC API at PUT /ers/config/ancendpoint/apply but receives a 403 Forbidden response. The credentials are correct and the user can log into the ISE GUI. What are two possible causes?
Answer
Two possible causes for 403 on ISE ERS:
ERS API is not enabled — The ERS API is disabled by default on ISE. It must be enabled at Administration → Settings → API Settings → ERS (Read/Write).
User lacks ERS Admin role — The API user must have the ERS Admin role assigned (not just regular admin). This is configured under Administration → System → Admin Access → Administrators.
Scenario 4 – Segmentation via VPN IDs
A network architect needs to segment traffic between departments across the WAN. The organization uses Cisco SD-WAN with vManage. Which segmentation model should they use, and what is the primary identifier?
Answer
SD-WAN uses VPN ID-based segmentation. Each VPN (0-65530) creates a separate routing table on vEdge/cEdge routers, providing traffic isolation across the WAN overlay. VPN 0 is the transport VPN, VPN 512 is management. Custom VPNs (e.g., VPN 10 for Engineering, VPN 20 for HR) provide departmental segmentation. This is configured via vManage templates at /template/policy/vsmart.
Scenario 5 – Identify the Error in This Compliance Script
The following script attempts to run a compliance check via Catalyst Center but fails with 401 Unauthorized on the Command Runner call. Identify the error.
The error is in the authentication header. Catalyst Center uses X-Auth-Token as the header name, notAuthorization: Bearer. The corrected headers should be:
This is a very common exam question — Catalyst Center does NOT use Bearer token authentication.
Scenario 6 – ISE Returns XML Instead of JSON
A script calls GET /ers/config/sgt on ISE and response.json() throws a JSONDecodeError. The request returns status 200 and data is present in response.text. What is wrong?
Answer
ISE ERS API defaults to XML responses. The script is missing the Accept: application/json header. Fix: