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.
- package/EXAMPLES.md +46 -0
- package/RADIUS.md +19 -0
- package/README.md +222 -0
- package/bin/cisco-ise.js +14 -0
- package/cli/commands/auth-profile.js +36 -0
- package/cli/commands/config.js +140 -0
- package/cli/commands/deployment.js +49 -0
- package/cli/commands/endpoint.js +167 -0
- package/cli/commands/guest.js +220 -0
- package/cli/commands/identity-group.js +24 -0
- package/cli/commands/internal-user.js +162 -0
- package/cli/commands/network-device.js +167 -0
- package/cli/commands/radius.js +326 -0
- package/cli/commands/session.js +123 -0
- package/cli/commands/tacacs.js +125 -0
- package/cli/commands/trustsec.js +37 -0
- package/cli/formatters/csv.js +10 -0
- package/cli/formatters/json.js +5 -0
- package/cli/formatters/table.js +29 -0
- package/cli/formatters/toon.js +6 -0
- package/cli/index.js +44 -0
- package/cli/utils/api.js +297 -0
- package/cli/utils/audit.js +30 -0
- package/cli/utils/config.js +125 -0
- package/cli/utils/confirm.js +34 -0
- package/cli/utils/connection.js +47 -0
- package/cli/utils/failure-reasons.js +2086 -0
- package/cli/utils/mac.js +18 -0
- package/cli/utils/output.js +42 -0
- package/cli/utils/spinner.js +19 -0
- package/cli/utils/time.js +21 -0
- package/cli/utils/wordlist.js +9 -0
- package/docs/PHASES.md +38 -0
- package/package.json +45 -0
- package/skills/cisco-ise-cli/SKILL.md +346 -0
- package/test/cli/api.test.js +67 -0
- package/test/cli/audit.test.js +31 -0
- package/test/cli/config.test.js +60 -0
- package/test/cli/confirm.test.js +34 -0
- package/test/cli/connection.test.js +54 -0
- package/test/cli/formatters.test.js +41 -0
- package/test/cli/mac.test.js +37 -0
- package/test/cli/time.test.js +30 -0
- package/test/integration/ise.test.js +425 -0
package/cli/utils/mac.js
ADDED
|
@@ -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
|
+
});
|