cisco-ise 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/EXAMPLES.md +46 -0
  2. package/RADIUS.md +19 -0
  3. package/README.md +222 -0
  4. package/bin/cisco-ise.js +14 -0
  5. package/cli/commands/auth-profile.js +36 -0
  6. package/cli/commands/config.js +140 -0
  7. package/cli/commands/deployment.js +49 -0
  8. package/cli/commands/endpoint.js +167 -0
  9. package/cli/commands/guest.js +220 -0
  10. package/cli/commands/identity-group.js +24 -0
  11. package/cli/commands/internal-user.js +162 -0
  12. package/cli/commands/network-device.js +167 -0
  13. package/cli/commands/radius.js +326 -0
  14. package/cli/commands/session.js +123 -0
  15. package/cli/commands/tacacs.js +125 -0
  16. package/cli/commands/trustsec.js +37 -0
  17. package/cli/formatters/csv.js +10 -0
  18. package/cli/formatters/json.js +5 -0
  19. package/cli/formatters/table.js +29 -0
  20. package/cli/formatters/toon.js +6 -0
  21. package/cli/index.js +44 -0
  22. package/cli/utils/api.js +297 -0
  23. package/cli/utils/audit.js +30 -0
  24. package/cli/utils/config.js +125 -0
  25. package/cli/utils/confirm.js +34 -0
  26. package/cli/utils/connection.js +47 -0
  27. package/cli/utils/failure-reasons.js +2086 -0
  28. package/cli/utils/mac.js +18 -0
  29. package/cli/utils/output.js +42 -0
  30. package/cli/utils/spinner.js +19 -0
  31. package/cli/utils/time.js +21 -0
  32. package/cli/utils/wordlist.js +9 -0
  33. package/docs/PHASES.md +38 -0
  34. package/package.json +45 -0
  35. package/skills/cisco-ise-cli/SKILL.md +346 -0
  36. package/test/cli/api.test.js +67 -0
  37. package/test/cli/audit.test.js +31 -0
  38. package/test/cli/config.test.js +60 -0
  39. package/test/cli/confirm.test.js +34 -0
  40. package/test/cli/connection.test.js +54 -0
  41. package/test/cli/formatters.test.js +41 -0
  42. package/test/cli/mac.test.js +37 -0
  43. package/test/cli/time.test.js +30 -0
  44. package/test/integration/ise.test.js +425 -0
@@ -0,0 +1,18 @@
1
+ function normalizeMac(mac) {
2
+ const stripped = mac.replace(/[:\-\.]/g, "").toUpperCase();
3
+ if (stripped.length !== 12 || !/^[0-9A-F]{12}$/.test(stripped)) {
4
+ throw new Error(`Invalid MAC address: ${mac}`);
5
+ }
6
+ return stripped.match(/.{2}/g).join(":");
7
+ }
8
+
9
+ function isValidMac(mac) {
10
+ try {
11
+ normalizeMac(mac);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ module.exports = { normalizeMac, isValidMac };
@@ -0,0 +1,42 @@
1
+ const formatTable = require("../formatters/table.js");
2
+ const formatJson = require("../formatters/json.js");
3
+ const formatToon = require("../formatters/toon.js");
4
+ const formatCsv = require("../formatters/csv.js");
5
+
6
+ const formatters = { table: formatTable, json: formatJson, toon: formatToon, csv: formatCsv };
7
+
8
+ async function printResult(data, format) {
9
+ const formatter = formatters[format || "table"];
10
+ if (!formatter) throw new Error(`Unknown format "${format}". Valid: table, json, toon, csv`);
11
+ const output = await Promise.resolve(formatter(data));
12
+ console.log(output);
13
+ }
14
+
15
+ function printError(err) {
16
+ const message = err.message || String(err);
17
+ process.stderr.write(`Error: ${message}\n`);
18
+ if (message.includes("Authentication failed") || message.includes("401")) {
19
+ process.stderr.write('Hint: Run "cisco-ise config test" to verify your credentials.\n');
20
+ }
21
+ process.exitCode = 1;
22
+ }
23
+
24
+ function printDryRun(info) {
25
+ process.stderr.write("DRY RUN — no changes made\n");
26
+ process.stderr.write(`${info.method} ${info.url}\n`);
27
+ if (info.body) process.stderr.write(JSON.stringify(info.body, null, 2) + "\n");
28
+ }
29
+
30
+ function cleanResources(resources) {
31
+ if (!Array.isArray(resources)) return resources;
32
+ return resources.map((r) => {
33
+ const cleaned = {};
34
+ for (const [key, value] of Object.entries(r)) {
35
+ if (key === "link") continue;
36
+ cleaned[key] = value;
37
+ }
38
+ return cleaned;
39
+ });
40
+ }
41
+
42
+ module.exports = { printResult, printError, printDryRun, cleanResources };
@@ -0,0 +1,19 @@
1
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
2
+
3
+ function createSpinner(message = "Loading...") {
4
+ if (!process.stderr.isTTY) return { stop() {} };
5
+
6
+ let i = 0;
7
+ const timer = setInterval(() => {
8
+ process.stderr.write(`\r${frames[i++ % frames.length]} ${message}`);
9
+ }, 80);
10
+
11
+ return {
12
+ stop(clear = true) {
13
+ clearInterval(timer);
14
+ if (clear) process.stderr.write("\r" + " ".repeat(message.length + 4) + "\r");
15
+ },
16
+ };
17
+ }
18
+
19
+ module.exports = { createSpinner };
@@ -0,0 +1,21 @@
1
+ const UNITS = { m: 60 * 1000, h: 60 * 60 * 1000, d: 24 * 60 * 60 * 1000 };
2
+
3
+ function parseDuration(str) {
4
+ const match = str.match(/^(\d+)([mhd])$/);
5
+ if (!match) throw new Error(`Invalid duration: "${str}". Use format like 30m, 2h, 1d`);
6
+ return parseInt(match[1], 10) * UNITS[match[2]];
7
+ }
8
+
9
+ function resolveTimeRange(opts) {
10
+ if (opts.last) {
11
+ const ms = parseDuration(opts.last);
12
+ const to = Date.now();
13
+ return { from: to - ms, to };
14
+ }
15
+ if (opts.from && opts.to) {
16
+ return { from: new Date(opts.from).getTime(), to: new Date(opts.to).getTime() };
17
+ }
18
+ throw new Error("Specify --last <duration> or --from <date> --to <date>");
19
+ }
20
+
21
+ module.exports = { parseDuration, resolveTimeRange };
@@ -0,0 +1,9 @@
1
+ const crypto = require("crypto");
2
+
3
+ function getRandomWord() {
4
+ // Generate a random 8-char string that doesn't exist in the codebase
5
+ // Not guessable, not brute-forceable from a known word list
6
+ return crypto.randomBytes(4).toString("hex");
7
+ }
8
+
9
+ module.exports = { getRandomWord };
package/docs/PHASES.md ADDED
@@ -0,0 +1,38 @@
1
+ # cisco-ise Roadmap
2
+
3
+ ## Phase 1: Core CLI
4
+
5
+ - Config management (multi-cluster, ss-cli, read-only protection)
6
+ - Endpoint commands (list, search, add, update, delete, bulk CSV)
7
+ - Guest commands (create, extend, suspend, reinstate, delete, portals)
8
+ - Network device commands (list, search, add, update, delete)
9
+ - Session commands (list, search, disconnect, reauth/CoA)
10
+ - RADIUS commands (failures with human-readable reasons, live polling)
11
+ - TACACS commands (failures, live polling, command sets, profiles)
12
+ - Read-only groups: identity-group, auth-profile, trustsec, deployment
13
+ - MAC address normalization (any format accepted)
14
+ - Identifier resolution (MACs/names → ISE UUIDs internally)
15
+ - Response caching (5min TTL, per-cluster)
16
+ - Rate limiting (exponential backoff on 429)
17
+ - Pagination (auto-paginate with --limit safety)
18
+ - Dry-run for all write operations
19
+ - Human-in-the-loop random word confirmation for read-only clusters
20
+ - Output formats: table, json, toon, csv
21
+ - Audit trail (JSONL with rotation)
22
+ - skills.sh skill for AI agents
23
+ - update-notifier
24
+
25
+ ## Phase 2: Analysis & Bulk Provisioning
26
+
27
+ - `cisco-ise apply --file setup.yaml [--dry-run]` — config-driven bulk operations
28
+ - YAML/JSON files define multiple operations (endpoints, groups, NADs)
29
+ - Sequential execution with rollback on failure
30
+ - Bulk delete and bulk update via CSV
31
+ - Interactive REPL mode with autocomplete
32
+
33
+ ## Phase 3: Advanced Integration
34
+
35
+ - ISE pxGrid integration for real-time context sharing
36
+ - Webhook/event triggers
37
+ - Policy set management
38
+ - Cross-platform reporting (combine with cisco-axl, cisco-dime data)
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "cisco-ise",
3
+ "version": "1.0.0",
4
+ "description": "CLI for Cisco ISE (Identity Services Engine) - ERS and OpenAPI operations",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "cisco-ise": "./bin/cisco-ise.js"
8
+ },
9
+ "scripts": {
10
+ "test": "node --test test/cli/*.test.js",
11
+ "test:integration": "NODE_ENV=test node --test test/integration/ise.test.js"
12
+ },
13
+ "keywords": [
14
+ "cisco",
15
+ "ise",
16
+ "identity-services-engine",
17
+ "ers",
18
+ "network-access",
19
+ "cli"
20
+ ],
21
+ "author": "sieteunoseis (jeremy.worden@gmail.com)",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/sieteunoseis/cisco-ise.git"
26
+ },
27
+ "funding": {
28
+ "type": "buymeacoffee",
29
+ "url": "https://buymeacoffee.com/automatebldrs"
30
+ },
31
+ "dependencies": {
32
+ "@toon-format/toon": "^2.1.0",
33
+ "axios": "^1.13.6",
34
+ "cli-table3": "^0.6.5",
35
+ "commander": "^14.0.3",
36
+ "csv-parse": "^6.2.1",
37
+ "csv-stringify": "^6.7.0",
38
+ "fast-xml-parser": "^5.5.8",
39
+ "update-notifier": "^7.3.1"
40
+ },
41
+ "devDependencies": {
42
+ "dotenv": "^17.3.1",
43
+ "envalid": "^8.1.1"
44
+ }
45
+ }
@@ -0,0 +1,346 @@
1
+ ---
2
+ name: cisco-ise-cli
3
+ description: Use when managing Cisco ISE via the cisco-ise CLI — endpoints, guests, network devices, sessions, RADIUS/TACACS monitoring, and identity management operations.
4
+ ---
5
+
6
+ # cisco-ise CLI
7
+
8
+ CLI for Cisco ISE (Identity Services Engine) 3.1+ targeting day-to-day operations and troubleshooting.
9
+
10
+ ## Setup
11
+
12
+ Configure a cluster (one-time):
13
+
14
+ ```bash
15
+ cisco-ise config add <name> --host <host> --username <user> --password '<ss:ID:password>' --insecure
16
+ cisco-ise config test
17
+ ```
18
+
19
+ Or use environment variables:
20
+
21
+ ```bash
22
+ export CISCO_ISE_HOST=<host>
23
+ export CISCO_ISE_USERNAME=<user>
24
+ export CISCO_ISE_PASSWORD=<password>
25
+ ```
26
+
27
+ Secret Server references are supported: `<ss:ID:field>` (requires ss-cli).
28
+
29
+ ## Command Groups
30
+
31
+ | Command | Description |
32
+ |---------|-------------|
33
+ | `config` | Manage ISE cluster configurations (add/use/list/show/remove/test/update/clear-cache) |
34
+ | `endpoint` | Manage endpoints (list/search/add/update/delete, CSV bulk) |
35
+ | `guest` | Manage guest users (list/search/create/extend/suspend/reinstate/delete/portals) |
36
+ | `network-device` | Manage NADs (list/search/get/add/update/delete) |
37
+ | `session` | Active sessions (list/search/disconnect/reauth) |
38
+ | `radius` | RADIUS monitoring (failures with human-readable reasons, live polling) |
39
+ | `tacacs` | TACACS+ monitoring (failures/live/command-sets/profiles) |
40
+ | `identity-group` | List identity groups (--type endpoint/user) |
41
+ | `auth-profile` | List/get authorization profiles |
42
+ | `trustsec` | TrustSec SGTs and SGACLs (read-only) |
43
+ | `deployment` | ISE deployment nodes and status (read-only) |
44
+
45
+ ## Common Workflows
46
+
47
+ ### List all endpoints
48
+
49
+ ```bash
50
+ cisco-ise endpoint list --insecure
51
+ cisco-ise endpoint search --mac AA:BB:CC:DD:EE:FF --insecure
52
+ ```
53
+
54
+ ### Add an endpoint (with dry-run)
55
+
56
+ ```bash
57
+ cisco-ise endpoint add --mac AA:BB:CC:DD:EE:FF --group "Profiled" --dry-run --insecure
58
+ cisco-ise endpoint add --csv endpoints.csv --insecure
59
+ ```
60
+
61
+ ### Check RADIUS authentication failures
62
+
63
+ ```bash
64
+ cisco-ise radius failures --last 1h --insecure
65
+ cisco-ise radius failures --last 30m --user jdoe --insecure
66
+ ```
67
+
68
+ ### Live RADIUS monitoring
69
+
70
+ ```bash
71
+ cisco-ise radius live --insecure
72
+ ```
73
+
74
+ ### Manage guest users
75
+
76
+ ```bash
77
+ cisco-ise guest portals --insecure
78
+ cisco-ise guest create --first "John" --last "Doe" --email "john@example.com" --portal "Sponsored Guest Portal (default)" --insecure
79
+ cisco-ise guest list --insecure
80
+ ```
81
+
82
+ ### Check active sessions
83
+
84
+ ```bash
85
+ cisco-ise session list --insecure
86
+ cisco-ise session search --mac E2:7C:7E:5B:F0:E0 --insecure
87
+ cisco-ise session disconnect E2:7C:7E:5B:F0:E0 --insecure
88
+ ```
89
+
90
+ ### Manage network devices
91
+
92
+ ```bash
93
+ cisco-ise network-device list --insecure
94
+ cisco-ise network-device add --name "switch01" --ip 10.0.0.1 --radius-secret '<ss:ID:radius-secret>' --insecure
95
+ ```
96
+
97
+ ### View deployment info
98
+
99
+ ```bash
100
+ cisco-ise deployment nodes --insecure
101
+ cisco-ise deployment status --insecure
102
+ ```
103
+
104
+ ## MAC Address Formats
105
+
106
+ Any common format is accepted and automatically normalized:
107
+ - `AA:BB:CC:DD:EE:FF` (colon-separated)
108
+ - `AA-BB-CC-DD-EE-FF` (dash-separated)
109
+ - `AABB.CCDD.EEFF` (Cisco dot notation)
110
+ - `aabbccddeeff` (bare hex)
111
+
112
+ ## Output Formats
113
+
114
+ - `--format table` (default) — human-readable
115
+ - `--format json` — for scripting/parsing
116
+ - `--format toon` — token-efficient for AI agents (recommended)
117
+ - `--format csv` — for spreadsheets
118
+
119
+ ## Key Flags
120
+
121
+ - `--insecure` — required for self-signed ISE certs (most environments)
122
+ - `--dry-run` — show HTTP method, URL, and payload without executing
123
+ - `--read-only` — block all write operations (human-in-the-loop confirmation)
124
+ - `--cluster <name>` — target a specific cluster
125
+ - `--no-cache` — bypass 5-minute response cache
126
+ - `--debug` — enable verbose logging
127
+ - `--no-audit` — disable audit trail logging
128
+
129
+ ## RADIUS Troubleshooting Workflow
130
+
131
+ When a user reports a connectivity or authentication problem, follow this workflow. Always use `--format json` so you can parse results programmatically.
132
+
133
+ ### Step 1: Identify the device
134
+
135
+ Get the MAC address from the user, or find it from active sessions:
136
+
137
+ ```bash
138
+ cisco-ise session list --format json
139
+ cisco-ise session search --mac <mac> --format json
140
+ cisco-ise session search --user <username> --format json
141
+ ```
142
+
143
+ ### Step 2: Run troubleshoot
144
+
145
+ ```bash
146
+ cisco-ise radius troubleshoot --mac <mac> --last 1d --format json
147
+ ```
148
+
149
+ This returns the full auth history with: pass/fail, matched policy rules, failure reasons, auth protocol, VLAN assignment, and ISE server.
150
+
151
+ ### Step 3: Analyze and correlate
152
+
153
+ Based on the troubleshoot output, run follow-up commands:
154
+
155
+ **If auth is failing — check the user:**
156
+ ```bash
157
+ cisco-ise internal-user get <username> --format json
158
+ ```
159
+ - Is `enabled` false? → `cisco-ise internal-user update <user> --enable`
160
+ - Is the user missing? → `cisco-ise internal-user add --user-name <user> --user-password <pass> --group <group>`
161
+
162
+ **If NAD not found (11007) — check network device:**
163
+ ```bash
164
+ cisco-ise network-device list --format json
165
+ ```
166
+ - Is the device missing? → `cisco-ise network-device add --name <name> --ip <ip> --radius-secret <secret>`
167
+
168
+ **If shared secret mismatch (11036, 22040) — verify NAD config:**
169
+ ```bash
170
+ cisco-ise network-device get <device-name> --format json
171
+ ```
172
+ - Check `authenticationSettings.radiusSharedSecret` matches the device config.
173
+
174
+ **If CoA failing (5417, 11213) — check CoA port:**
175
+ ```bash
176
+ cisco-ise network-device get <device-name> --format json
177
+ ```
178
+ - Check `coaPort`. Cisco uses 1700, RFC standard is 3799. UniFi uses 3799.
179
+
180
+ **If certificate rejected (12520) — check deployment:**
181
+ ```bash
182
+ cisco-ise deployment nodes --format json
183
+ ```
184
+ - Verify EAP certificate. Client must trust the signing CA.
185
+
186
+ **If authorization denied (15039) — check policy:**
187
+ ```bash
188
+ cisco-ise auth-profile list --format json
189
+ cisco-ise identity-group list --format json
190
+ ```
191
+ - User may not match any authorization rule. Check group membership.
192
+
193
+ ### Step 4: Verify the fix
194
+
195
+ After making changes, ask the user to reconnect, then re-run:
196
+ ```bash
197
+ cisco-ise radius troubleshoot --mac <mac> --last 30m
198
+ ```
199
+ Confirm the latest auth shows PASS.
200
+
201
+ ### Step 5: Common failure code reference
202
+
203
+ | Code | Issue | Quick Fix |
204
+ |------|-------|-----------|
205
+ | 5411 | EAP timeout — no client response | Check supplicant config, certificate trust |
206
+ | 5417 | CoA failed | Check NAD CoA port and shared secret |
207
+ | 11007 | NAD not found | Add NAD: `cisco-ise network-device add` |
208
+ | 11036 | Invalid Message-Authenticator | Shared secret mismatch — update NAD |
209
+ | 12520 | Client rejected ISE cert | Install CA cert on client or fix ISE EAP cert |
210
+ | 15039 | Rejected by authz profile | Add authorization rule for this user/group |
211
+ | 22040 | Wrong password or shared secret | Reset password or fix shared secret |
212
+ | 22056 | User not found | Add user or check auth policy identity store |
213
+ | 22061 | User disabled | `cisco-ise internal-user update <user> --enable` |
214
+ | 24408 | AD auth failed — wrong password | Check AD credentials, also check shared secret if PAP |
215
+
216
+ The CLI has 311 ISE failure codes mapped in `cli/utils/failure-reasons.js` with causes and remediation. The `radius troubleshoot` command outputs these automatically.
217
+
218
+ ## Internal User Management
219
+
220
+ ```bash
221
+ cisco-ise internal-user list
222
+ cisco-ise internal-user get <name>
223
+ cisco-ise internal-user add --user-name <name> --user-password <pass> --group <group>
224
+ cisco-ise internal-user update <name> --enable|--disable|--group <group>|--user-password <pass>
225
+ cisco-ise internal-user delete <name>
226
+ ```
227
+
228
+ ## Agent-Safe Deployment
229
+
230
+ When giving AI agents access to the cisco-ise CLI, use these layers of protection. Only ISE-side RBAC is truly unbypassable — the others add friction but a determined agent with shell access could work around them.
231
+
232
+ ### Layer 1: ISE Read-Only Admin (recommended — unbypassable)
233
+
234
+ Create a dedicated ISE admin account with **ERS Operator** (read-only) instead of **ERS Admin** (read-write). The ISE server itself rejects all write API calls regardless of what the CLI or agent does.
235
+
236
+ In ISE admin GUI:
237
+ 1. Administration > System > Admin Access > Administrators > Admin Users
238
+ 2. Create a new admin (e.g., `cli-reader`)
239
+ 3. Assign to **ERS Operator** group (read-only ERS access)
240
+ 4. Optionally add **MNT Admin** for monitoring/troubleshooting
241
+
242
+ ```bash
243
+ cisco-ise config add prod --host <host> --username cli-reader --password '<ss:ID:password>' --insecure
244
+ ```
245
+
246
+ This is the only protection that cannot be bypassed by any client-side mechanism. The ISE server enforces the restriction.
247
+
248
+ For write operations, use a separate cluster config with ERS Admin credentials that only humans access:
249
+ ```bash
250
+ cisco-ise config add prod-admin --host <host> --username cli-admin --password '<ss:ID:password>' --read-only --insecure
251
+ ```
252
+
253
+ ### Layer 2: CLI Read-Only Flag (human-in-the-loop)
254
+
255
+ ```bash
256
+ cisco-ise config add prod --host <host> --username <user> --password '<ss:ID:password>' --read-only --insecure
257
+ ```
258
+
259
+ Write operations require typing a random 8-character hex string in an interactive TTY. Non-interactive environments (agents, scripts, pipes) are blocked entirely because `process.stdin.isTTY` is false.
260
+
261
+ **Limitation:** An agent with shell access could edit `~/.cisco-ise/config.json` directly to remove the `readOnly` flag.
262
+
263
+ ### Layer 3: Separate Credentials
264
+
265
+ Keep admin/write credentials in Secret Server, not in the config file:
266
+ ```bash
267
+ cisco-ise config add prod --host <host> --username <user> --password '<ss:ID:password>' --insecure
268
+ ```
269
+
270
+ The agent never sees the actual password — it's resolved at runtime from Secret Server via `ss-cli`. Rotate credentials in Secret Server without touching the CLI config.
271
+
272
+ ### Recommended Setup for Agent Access
273
+
274
+ | Account | ISE Role | CLI Config | Used By |
275
+ |---------|----------|------------|---------|
276
+ | `cli-reader` | ERS Operator + MNT Admin | `prod` cluster | AI agents (read + troubleshoot) |
277
+ | `cli-admin` | ERS Admin | `prod-admin` cluster, `--read-only` | Humans only (writes need TTY confirmation) |
278
+ | `sponsor` | Sponsor (internal user) | `--sponsor-user` in config | Guest management |
279
+
280
+ ### Data Exposure by Command
281
+
282
+ Before granting agent access, understand what data each command exposes. Use this to decide which ISE RBAC permissions to grant.
283
+
284
+ **Low sensitivity — safe for most agents:**
285
+
286
+ | Command | Data Exposed |
287
+ |---------|-------------|
288
+ | `deployment nodes` | ISE hostnames, roles, services |
289
+ | `deployment status` | Node status |
290
+ | `identity-group list` | Group names and descriptions |
291
+ | `auth-profile list` | Authorization profile names |
292
+ | `trustsec sgt list` | SGT names and descriptions |
293
+ | `trustsec sgacl list` | SGACL names |
294
+ | `tacacs command-sets` | TACACS command set names |
295
+ | `tacacs profiles` | TACACS profile names |
296
+
297
+ **Medium sensitivity — contains user/device identifiers:**
298
+
299
+ | Command | Data Exposed |
300
+ |---------|-------------|
301
+ | `endpoint list/search` | MAC addresses, endpoint group membership |
302
+ | `session list/search` | Active users, MAC addresses, NAS IPs, ISE server |
303
+ | `radius auth-log` | Auth history: usernames, MACs, pass/fail, timestamps, policy matches |
304
+ | `radius troubleshoot` | Full auth detail: all of auth-log plus protocol, TLS version, VLAN, identity store |
305
+ | `radius failures` | Failed auth attempts with usernames and failure reasons |
306
+ | `internal-user list` | Usernames, descriptions, user IDs |
307
+ | `guest list` | Guest usernames and IDs |
308
+
309
+ **High sensitivity — contains secrets or enables write operations:**
310
+
311
+ | Command | Data Exposed / Risk |
312
+ |---------|-------------|
313
+ | `network-device get` | **RADIUS shared secrets in plaintext**, device IPs, CoA ports |
314
+ | `config show` | ISE hostname, admin username, masked password, sponsor username |
315
+ | `internal-user get` | User details including email, group membership, enabled status |
316
+ | `auth-profile get` | Full authorization profile config (VLANs, ACLs, attributes) |
317
+ | `endpoint add/update/delete` | **Write operation** — modifies endpoint database |
318
+ | `network-device add/update/delete` | **Write operation** — modifies NAD database, exposes shared secrets |
319
+ | `internal-user add/update/delete` | **Write operation** — creates/modifies/removes user accounts |
320
+ | `guest create/delete` | **Write operation** — creates/removes guest accounts |
321
+ | `session disconnect/reauth` | **Write operation** — disrupts active user sessions |
322
+
323
+ ### Recommended Agent Scoping
324
+
325
+ **Troubleshooting-only agent (most common):**
326
+ - ISE role: ERS Operator + MNT Admin
327
+ - Safe commands: `session list/search`, `radius auth-log`, `radius troubleshoot`, `endpoint list/search`, `identity-group list`, `deployment nodes`
328
+ - Risk: exposes usernames, MACs, auth history — acceptable for helpdesk/NOC use
329
+
330
+ **Read-all agent (full visibility):**
331
+ - ISE role: ERS Operator + MNT Admin
332
+ - All read commands including `network-device get` (exposes shared secrets)
333
+ - Risk: shared secrets visible — use only if agent environment is trusted
334
+
335
+ **Full access agent (not recommended for production):**
336
+ - ISE role: ERS Admin
337
+ - All commands including writes
338
+ - Risk: agent can modify ISE configuration — use `--read-only` flag as a speed bump only
339
+
340
+ ### Audit Trail
341
+
342
+ Every CLI command is logged to `~/.cisco-ise/audit.jsonl` with timestamp, command, and cluster name. Review agent activity with:
343
+
344
+ ```bash
345
+ cat ~/.cisco-ise/audit.jsonl | tail -20
346
+ ```
@@ -0,0 +1,67 @@
1
+ const { describe, it, beforeEach, afterEach } = require("node:test");
2
+ const assert = require("node:assert");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ describe("ISE API client", () => {
8
+ let tmpDir, IseClient;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cisco-ise-api-"));
12
+ process.env.CISCO_ISE_CONFIG_DIR = tmpDir;
13
+ delete require.cache[require.resolve("../../cli/utils/api.js")];
14
+ IseClient = require("../../cli/utils/api.js");
15
+ });
16
+
17
+ afterEach(() => {
18
+ fs.rmSync(tmpDir, { recursive: true, force: true });
19
+ delete process.env.CISCO_ISE_CONFIG_DIR;
20
+ });
21
+
22
+ it("constructs ERS URL correctly", () => {
23
+ const client = new IseClient({ host: "ise.example.com", username: "u", password: "p" });
24
+ assert.equal(client.ersUrl("/endpoint"), "https://ise.example.com:9060/ers/config/endpoint");
25
+ });
26
+
27
+ it("constructs OpenAPI URL correctly", () => {
28
+ const client = new IseClient({ host: "ise.example.com", username: "u", password: "p" });
29
+ assert.equal(client.openApiUrl("/deployment/node"), "https://ise.example.com/api/v1/deployment/node");
30
+ });
31
+
32
+ it("constructs MNT URL correctly", () => {
33
+ const client = new IseClient({ host: "ise.example.com", username: "u", password: "p" });
34
+ assert.equal(client.mntUrl("/Session/ActiveList"), "https://ise.example.com/admin/API/mnt/Session/ActiveList");
35
+ });
36
+
37
+ it("generates cache key from URL and params", () => {
38
+ const client = new IseClient({ host: "ise.example.com", username: "u", password: "p" });
39
+ const key1 = client.cacheKey("/endpoint", { size: 100 });
40
+ const key2 = client.cacheKey("/endpoint", { size: 100 });
41
+ const key3 = client.cacheKey("/endpoint", { size: 50 });
42
+ assert.equal(key1, key2);
43
+ assert.notEqual(key1, key3);
44
+ });
45
+
46
+ it("routes ERS/OpenAPI to PPAN node when configured", () => {
47
+ const client = new IseClient({ host: "ise.example.com", ppan: "pan.example.com", username: "u", password: "p" });
48
+ assert.equal(client.ersUrl("/endpoint"), "https://pan.example.com:9060/ers/config/endpoint");
49
+ assert.equal(client.openApiUrl("/deployment/node"), "https://pan.example.com/api/v1/deployment/node");
50
+ // MNT should still use default host
51
+ assert.equal(client.mntUrl("/Session/ActiveList"), "https://ise.example.com/admin/API/mnt/Session/ActiveList");
52
+ });
53
+
54
+ it("routes MNT to PMNT node when configured", () => {
55
+ const client = new IseClient({ host: "ise.example.com", pmnt: "mnt.example.com", username: "u", password: "p" });
56
+ assert.equal(client.mntUrl("/Session/ActiveList"), "https://mnt.example.com/admin/API/mnt/Session/ActiveList");
57
+ // ERS/OpenAPI should still use default host
58
+ assert.equal(client.ersUrl("/endpoint"), "https://ise.example.com:9060/ers/config/endpoint");
59
+ });
60
+
61
+ it("routes to both PPAN and PMNT when both configured", () => {
62
+ const client = new IseClient({ host: "ise.example.com", ppan: "pan.example.com", pmnt: "mnt.example.com", username: "u", password: "p" });
63
+ assert.equal(client.ersUrl("/endpoint"), "https://pan.example.com:9060/ers/config/endpoint");
64
+ assert.equal(client.openApiUrl("/deployment/node"), "https://pan.example.com/api/v1/deployment/node");
65
+ assert.equal(client.mntUrl("/Session/ActiveList"), "https://mnt.example.com/admin/API/mnt/Session/ActiveList");
66
+ });
67
+ });
@@ -0,0 +1,31 @@
1
+ const { describe, it, beforeEach, afterEach } = require("node:test");
2
+ const assert = require("node:assert");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ describe("audit", () => {
8
+ let tmpDir, audit;
9
+
10
+ beforeEach(() => {
11
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "cisco-ise-audit-"));
12
+ process.env.CISCO_ISE_CONFIG_DIR = tmpDir;
13
+ delete require.cache[require.resolve("../../cli/utils/audit.js")];
14
+ delete require.cache[require.resolve("../../cli/utils/config.js")];
15
+ audit = require("../../cli/utils/audit.js");
16
+ });
17
+
18
+ afterEach(() => {
19
+ fs.rmSync(tmpDir, { recursive: true, force: true });
20
+ delete process.env.CISCO_ISE_CONFIG_DIR;
21
+ });
22
+
23
+ it("writes an audit entry", () => {
24
+ audit.log({ command: "endpoint list", cluster: "lab", status: "success" });
25
+ const file = path.join(tmpDir, "audit.jsonl");
26
+ assert.ok(fs.existsSync(file));
27
+ const entry = JSON.parse(fs.readFileSync(file, "utf8").trim());
28
+ assert.equal(entry.command, "endpoint list");
29
+ assert.ok(entry.timestamp);
30
+ });
31
+ });