@brunsforge/aarm 0.1.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/README.md +1180 -0
- package/dist/aarm.cjs +39260 -0
- package/dist/aarm.mjs +39262 -0
- package/dist/commands/apps.d.ts +2 -0
- package/dist/commands/apps.js +32 -0
- package/dist/commands/apps.js.map +1 -0
- package/dist/commands/preflight.d.ts +2 -0
- package/dist/commands/preflight.js +160 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/report.d.ts +2 -0
- package/dist/commands/report.js +166 -0
- package/dist/commands/report.js.map +1 -0
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +88 -0
- package/dist/commands/secrets.js.map +1 -0
- package/dist/commands/tenants.d.ts +2 -0
- package/dist/commands/tenants.js +209 -0
- package/dist/commands/tenants.js.map +1 -0
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +203 -0
- package/dist/commands/usage.js.map +1 -0
- package/dist/config/ConfigStore.d.ts +12 -0
- package/dist/config/ConfigStore.js +53 -0
- package/dist/config/ConfigStore.js.map +1 -0
- package/dist/config/CredentialStore.d.ts +8 -0
- package/dist/config/CredentialStore.js +29 -0
- package/dist/config/CredentialStore.js.map +1 -0
- package/dist/config/HistoryStore.d.ts +19 -0
- package/dist/config/HistoryStore.js +64 -0
- package/dist/config/HistoryStore.js.map +1 -0
- package/dist/exitCodes.d.ts +9 -0
- package/dist/exitCodes.js +10 -0
- package/dist/exitCodes.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +38 -0
- package/dist/index.js.map +1 -0
- package/dist/output/formatters.d.ts +10 -0
- package/dist/output/formatters.js +129 -0
- package/dist/output/formatters.js.map +1 -0
- package/dist/shared/context.d.ts +31 -0
- package/dist/shared/context.js +124 -0
- package/dist/shared/context.js.map +1 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,1180 @@
|
|
|
1
|
+
# aarm — Azure App Registration Monitor CLI
|
|
2
|
+
|
|
3
|
+
Command-line tool for monitoring Microsoft Entra App Registration client secrets. Lists all secrets across a tenant, identifies expiring and expired ones, assesses risk, and runs preflight checks to detect which permissions are available.
|
|
4
|
+
|
|
5
|
+
## What is aarm?
|
|
6
|
+
|
|
7
|
+
Client secrets on Microsoft Entra App Registrations expire silently. There is no built-in Microsoft view that shows all expiring secrets across a tenant, and there is no automatic renewal notification. Authentication failures caused by expired secrets are often discovered only after a production outage.
|
|
8
|
+
|
|
9
|
+
`aarm` solves this by:
|
|
10
|
+
|
|
11
|
+
- Authenticating against a tenant using the auth mode you choose (client secret, device code, interactive browser, certificate, or Azure CLI)
|
|
12
|
+
- Querying Microsoft Graph to list all App Registrations and their `passwordCredentials`
|
|
13
|
+
- Calculating expiry status and risk level for every secret
|
|
14
|
+
- Reporting findings as a readable table or as stable JSON for automation
|
|
15
|
+
|
|
16
|
+
It is part of the **Azure App Registration Monitor (AARM)** toolchain. The same core engine is used by the [AARM desktop UI](../../apps/maui-blazor/README.md). The CLI is independently useful in CI/CD pipelines, scheduled monitoring jobs, and on developer workstations.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Prerequisites
|
|
21
|
+
|
|
22
|
+
| Requirement | Details |
|
|
23
|
+
|---|---|
|
|
24
|
+
| Node.js | ≥ 18 |
|
|
25
|
+
| Microsoft Entra App Registration | Used to authenticate — must have admin consent granted |
|
|
26
|
+
| Graph permissions | At minimum: `Application.Read.All` (application permission) |
|
|
27
|
+
|
|
28
|
+
### Minimum permissions
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Microsoft Graph API (application permissions):
|
|
32
|
+
Application.Read.All — required for listing apps and secrets
|
|
33
|
+
Directory.Read.All — optional, needed for reading owners
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
> All application permissions require admin consent from a **Global Administrator** or **Privileged Role Administrator**.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### Option A — global install from npm (recommended for users)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install -g @brunsforge/aarm
|
|
46
|
+
aarm --version
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The binary lands at `%APPDATA%\npm\aarm.cmd` on Windows, or `/usr/local/bin/aarm` on macOS/Linux.
|
|
50
|
+
|
|
51
|
+
### Option B — run from source (for contributors or if the package is not yet published)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 1. Clone the monorepo
|
|
55
|
+
git clone https://github.com/brunsforge/AzureAppRegistrationSecretMonitor.git
|
|
56
|
+
cd AzureAppRegistrationSecretMonitor
|
|
57
|
+
|
|
58
|
+
# 2. Install all workspace dependencies (core + CLI)
|
|
59
|
+
npm install
|
|
60
|
+
|
|
61
|
+
# 3. Build the core library first, then the CLI
|
|
62
|
+
npm run build --workspace packages/core
|
|
63
|
+
npm run build --workspace packages/cli
|
|
64
|
+
|
|
65
|
+
# 4. Make aarm available on your PATH via npm link
|
|
66
|
+
cd packages/cli
|
|
67
|
+
npm link
|
|
68
|
+
|
|
69
|
+
# Verify
|
|
70
|
+
aarm --version
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
After `npm link`, any changes you make to `packages/cli/src` are active after the next `npm run build`.
|
|
74
|
+
|
|
75
|
+
If you do not want to use `npm link`, you can run the compiled script directly:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
node packages/cli/dist/index.js --version
|
|
79
|
+
|
|
80
|
+
# Or create a short alias in your shell profile:
|
|
81
|
+
alias aarm="node $(pwd)/packages/cli/dist/index.js"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Option C — run TypeScript directly without building (fastest for quick iteration)
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
npm install --workspace packages/cli # already done if you ran npm install at root
|
|
88
|
+
|
|
89
|
+
# Run a command directly from TypeScript source using tsx
|
|
90
|
+
cd packages/cli
|
|
91
|
+
npx tsx src/index.ts --version
|
|
92
|
+
npx tsx src/index.ts --tenant "Contoso PROD" secrets list
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
> **tsx** (TypeScript Execute) runs `.ts` files without a compile step. It is listed as a devDependency in `packages/cli`.
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## Quick local test walkthrough
|
|
100
|
+
|
|
101
|
+
This walks through the full first-run experience from source checkout to seeing real secrets.
|
|
102
|
+
|
|
103
|
+
### Step 1 — Prepare your App Registration
|
|
104
|
+
|
|
105
|
+
Before running any `aarm` command you need an App Registration in your Entra tenant with
|
|
106
|
+
the right permissions. The auth mode you choose determines which type of permission you need
|
|
107
|
+
(see [Permissions and App Registration Setup](#app-registration-setup--complete-azure-portal-reference) below for the full guide).
|
|
108
|
+
|
|
109
|
+
**Fastest path for a one-time test:** use `device-code` mode with a public client App Registration.
|
|
110
|
+
You sign in interactively in a browser — no client secret to manage.
|
|
111
|
+
|
|
112
|
+
Quick Azure Portal checklist:
|
|
113
|
+
|
|
114
|
+
1. Azure Portal → **Entra ID → App registrations → New registration**
|
|
115
|
+
- Name: `aarm-test`
|
|
116
|
+
- Account type: single tenant
|
|
117
|
+
- No redirect URI needed for device-code
|
|
118
|
+
2. **API permissions → Add → Microsoft Graph → Delegated → `Application.Read.All`**
|
|
119
|
+
3. **Grant admin consent** (requires Global Administrator)
|
|
120
|
+
4. **Authentication → Advanced settings → Allow public client flows: Yes**
|
|
121
|
+
5. Copy the **Application (client) ID** — you will need it in the next step
|
|
122
|
+
|
|
123
|
+
> **Do I also need a user role?** Yes for delegated (device-code / interactive-browser / azure-cli) modes.
|
|
124
|
+
> The signed-in user must have **Cloud Application Administrator** or **Application Administrator** in Entra to read all App Registrations tenant-wide. A user without these roles can only see apps they own.
|
|
125
|
+
|
|
126
|
+
### Step 2 — Add the tenant to aarm
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
aarm tenants add \
|
|
130
|
+
--tenant-id "<your-entra-tenant-id-guid>" \
|
|
131
|
+
--display-name "My Test Tenant" \
|
|
132
|
+
--auth-mode device-code \
|
|
133
|
+
--client-id "<app-registration-client-id>"
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Or run `aarm tenants add` without flags for the interactive prompt:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
Tenant ID (GUID): xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
140
|
+
Display name: My Test Tenant
|
|
141
|
+
Auth mode: device-code
|
|
142
|
+
Client ID (App Registration GUID): xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
143
|
+
Log Analytics Workspace ID (optional): <Enter>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Verify it was saved:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
aarm tenants list
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Step 3 — Run a preflight check
|
|
153
|
+
|
|
154
|
+
Before listing secrets, confirm that the permissions are working:
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
aarm --tenant "My Test Tenant" preflight run
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Expected output (all capabilities green):
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Authentication : OK
|
|
164
|
+
Graph reachable : OK
|
|
165
|
+
|
|
166
|
+
Capabilities
|
|
167
|
+
┌────────────────────────────────────┬──────────────┐
|
|
168
|
+
│ canReadApplications │ [✓] Available│
|
|
169
|
+
│ canReadApplicationSecrets │ [✓] Available│
|
|
170
|
+
│ canReadOwners │ [ ] Unavailable ← needs Directory.Read.All
|
|
171
|
+
│ ... │ │
|
|
172
|
+
└────────────────────────────────────┴──────────────┘
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
If `canReadApplications` or `canReadApplicationSecrets` shows `[ ] Unavailable`, run:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
aarm --tenant "My Test Tenant" preflight explain
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This prints the exact steps to grant the missing permission.
|
|
182
|
+
|
|
183
|
+
**Common first-run errors and fixes:**
|
|
184
|
+
|
|
185
|
+
| Error | Cause | Fix |
|
|
186
|
+
|---|---|---|
|
|
187
|
+
| `Authentication failed: ... Application not found` | Wrong tenant ID or client ID | Double-check both GUIDs in the Azure Portal |
|
|
188
|
+
| `Permission denied: Insufficient privileges` | `Application.Read.All` not granted or admin consent missing | Grant in Azure Portal → API permissions → admin consent |
|
|
189
|
+
| `Permission denied: Authorization_RequestDenied` | Signed-in user lacks Entra role | Assign Cloud Application Administrator or Application Administrator |
|
|
190
|
+
| Device code prompt never appears | Network / firewall blocking login.microsoft.com | Check connectivity |
|
|
191
|
+
|
|
192
|
+
### Step 4 — List secrets
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
# All secrets, colour-coded by risk
|
|
196
|
+
aarm --tenant "My Test Tenant" secrets list
|
|
197
|
+
|
|
198
|
+
# Only expiring within 90 days
|
|
199
|
+
aarm --tenant "My Test Tenant" secrets expiring --days 90
|
|
200
|
+
|
|
201
|
+
# Already expired
|
|
202
|
+
aarm --tenant "My Test Tenant" secrets expired
|
|
203
|
+
|
|
204
|
+
# JSON output for automation / scripts
|
|
205
|
+
aarm --tenant "My Test Tenant" secrets list --output json
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Step 5 — Try the apps and report commands
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# Per-app risk summary
|
|
212
|
+
aarm --tenant "My Test Tenant" apps list
|
|
213
|
+
|
|
214
|
+
# Full tenant summary report
|
|
215
|
+
aarm --tenant "My Test Tenant" report tenant-summary
|
|
216
|
+
|
|
217
|
+
# Markdown report of expiring secrets (copy into a ticket or email)
|
|
218
|
+
aarm --tenant "My Test Tenant" report expiring --days 30 --output markdown
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Step 6 — Switching to client-secret (unattended / CI use)
|
|
222
|
+
|
|
223
|
+
If you want to run without interactive sign-in (CI/CD, scheduled jobs), re-add the tenant
|
|
224
|
+
with `client-secret` mode. You will need a separate App Registration configured with
|
|
225
|
+
**Application** permissions (not Delegated) — see the [App Registration setup guide](#app-registration-setup--complete-azure-portal-reference) for the full steps.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Remove the device-code tenant first
|
|
229
|
+
aarm tenants remove "My Test Tenant"
|
|
230
|
+
|
|
231
|
+
# Re-add with client-secret
|
|
232
|
+
aarm tenants add \
|
|
233
|
+
--tenant-id "<tenant-id>" \
|
|
234
|
+
--display-name "My Test Tenant" \
|
|
235
|
+
--auth-mode client-secret \
|
|
236
|
+
--client-id "<daemon-app-client-id>"
|
|
237
|
+
# → aarm prompts for the client secret (stored in Windows Credential Manager)
|
|
238
|
+
|
|
239
|
+
# Run without any browser prompt
|
|
240
|
+
aarm --tenant "My Test Tenant" secrets list
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
## How authentication works — the two App Registrations
|
|
246
|
+
|
|
247
|
+
Before reading the mode details, understand the two distinct App Registrations involved:
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
Your Entra tenant
|
|
251
|
+
│
|
|
252
|
+
├── App Registration: "aarm" ← YOU CREATE THIS ONCE
|
|
253
|
+
│ This is aarm's own identity.
|
|
254
|
+
│ It holds the permissions to call Graph.
|
|
255
|
+
│ Its client-id is what you set via --client-id when running "aarm tenants add".
|
|
256
|
+
│
|
|
257
|
+
├── App Registration: "CRM Connector" ┐
|
|
258
|
+
├── App Registration: "Teams Bot" ├── aarm READS THESE — do not touch them
|
|
259
|
+
└── App Registration: "Export Worker" ┘
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
**You only need one "aarm" App Registration per tenant, regardless of auth mode.**
|
|
263
|
+
|
|
264
|
+
The auth mode only changes how `aarm` proves its identity to Entra when requesting a token. Once the token is issued, `aarm` uses it to call `GET /applications` and `GET /applications/{id}/passwordCredentials` to list all the other App Registrations and their secrets.
|
|
265
|
+
|
|
266
|
+
### Permission types: Application vs Delegated
|
|
267
|
+
|
|
268
|
+
Which permission type you need depends on the auth mode:
|
|
269
|
+
|
|
270
|
+
| Permission type | Auth modes that use it | What it means |
|
|
271
|
+
|---|---|---|
|
|
272
|
+
| **Application** | `client-secret`, `certificate` | `aarm` acts as itself (service principal). No user is involved. Can read everything if admin-consented. |
|
|
273
|
+
| **Delegated** | `device-code`, `username-password`, `interactive-browser` | `aarm` acts on behalf of the signed-in user. Access depends on the user's Entra roles. |
|
|
274
|
+
| **Neither** | `azure-cli` | Uses the token from `az login` directly. No separate App Registration required. |
|
|
275
|
+
|
|
276
|
+
> For **Delegated** modes, the signed-in user must have the **Cloud Application Administrator** or **Application Administrator** role in Entra to be able to read all App Registrations across the tenant. A regular user without these roles can only read apps they own.
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## Authentication modes — configuration reference
|
|
281
|
+
|
|
282
|
+
This section is critical. Picking the wrong mode causes the "az command not available" or "no token" errors you see on the first run.
|
|
283
|
+
|
|
284
|
+
### Mode comparison
|
|
285
|
+
|
|
286
|
+
| Mode | Who signs in | MFA support | Needs `az` CLI | Needs App Registration | Best for |
|
|
287
|
+
|---|---|---|---|---|---|
|
|
288
|
+
| `client-secret` | Service principal | N/A | No | Yes | CI/CD, automation, unattended |
|
|
289
|
+
| `username-password` | Your Entra user account (email + password) | **No** | No | Yes | Quick local access when no MFA is enforced |
|
|
290
|
+
| `device-code` | Your Entra user account (browser) | **Yes** | No | Yes | Interactive use, developer workstations |
|
|
291
|
+
| `interactive-browser` | Your Entra user account (browser redirect) | **Yes** | No | Yes | Desktop tools with localhost redirect |
|
|
292
|
+
| `certificate` | Service principal | N/A | No | Yes | High-security automated deployments |
|
|
293
|
+
| `azure-cli` | Reuses existing `az login` session | **Yes** | **Yes** | No | Developers who already use Azure CLI |
|
|
294
|
+
| `workload-identity-federation` | Managed Identity (Azure-hosted only) | N/A | No | Yes | **Not supported in the CLI.** Azure Function / VM only. |
|
|
295
|
+
|
|
296
|
+
> **`workload-identity-federation` is supported by the underlying library (`@brunsforge/azure-app-registration-monitor`) and by the AARM Azure Function, but it requires an Azure-hosted runtime (Function App, VM, ACI, AKS) and cannot be used with the `aarm` CLI on a developer workstation. Use `client-secret` or `certificate` for unattended automation, or `device-code` for interactive local use.
|
|
297
|
+
|
|
298
|
+
> **If you just got "az command not available":** your tenant is configured as `azure-cli` but the Azure CLI is not installed. Remove the tenant and re-add it with `device-code` or `username-password`.
|
|
299
|
+
|
|
300
|
+
---
|
|
301
|
+
|
|
302
|
+
## App Registration setup — complete Azure Portal reference
|
|
303
|
+
|
|
304
|
+
aarm needs **one App Registration** in each Entra tenant it monitors. This registration is aarm's own identity — it is separate from all the App Registrations that aarm reads and monitors.
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
Your Entra tenant
|
|
308
|
+
│
|
|
309
|
+
├── "aarm" ← you create this once per tenant
|
|
310
|
+
│ → holds permissions to call Microsoft Graph
|
|
311
|
+
│ → its client-id is what you set with aarm tenants add
|
|
312
|
+
│
|
|
313
|
+
├── "CRM Connector" ┐
|
|
314
|
+
├── "Teams Bot" ├─ aarm reads and monitors these
|
|
315
|
+
└── "Export Worker" ┘ (you never touch them for the setup)
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
The auth mode determines whether the token is issued to a **service principal** (application modes: `client-secret`, `certificate`) or to a **signed-in user** (delegated modes: `device-code`, `username-password`, `interactive-browser`, `azure-cli`). This matters because the required permission type and the error messages differ.
|
|
319
|
+
|
|
320
|
+
### Which permission type do I need?
|
|
321
|
+
|
|
322
|
+
| Auth mode | Token identity | Graph permission type | User role required? |
|
|
323
|
+
|---|---|---|---|
|
|
324
|
+
| `device-code` | Signed-in user | **Delegated** | Yes — Cloud App Admin |
|
|
325
|
+
| `username-password` | Signed-in user | **Delegated** | Yes — Cloud App Admin |
|
|
326
|
+
| `interactive-browser` | Signed-in user | **Delegated** | Yes — Cloud App Admin |
|
|
327
|
+
| `azure-cli` | Signed-in user (via az) | **Delegated** | Yes — Cloud App Admin |
|
|
328
|
+
| `client-secret` | Service principal | **Application** | No |
|
|
329
|
+
| `certificate` | Service principal | **Application** | No |
|
|
330
|
+
|
|
331
|
+
> **Why does the user role matter for delegated modes?**
|
|
332
|
+
> With delegated `Application.Read.All`, Entra checks both the permission grant *and* whether the signed-in user has an Entra directory role that allows reading all App Registrations. Without **Cloud Application Administrator** or **Application Administrator**, the user can only see apps they personally own — not all apps in the tenant.
|
|
333
|
+
> Application permissions (service principal) do not have this restriction: if admin consent is granted, the service principal can read all apps regardless of who created them.
|
|
334
|
+
---
|
|
335
|
+
### Recommended app registration setup
|
|
336
|
+
|
|
337
|
+
For local/user-based modes, create one public client app registration:
|
|
338
|
+
|
|
339
|
+
**AzureAppRegistrationSecretMonitor.PublicClient**
|
|
340
|
+
- Used by: device-code, interactive-browser, username-password
|
|
341
|
+
- Secret: none
|
|
342
|
+
- Certificate: none
|
|
343
|
+
- Redirect URI: `http://localhost` for interactive-browser
|
|
344
|
+
- Allow public client flows: Yes
|
|
345
|
+
- Microsoft Graph delegated permissions:
|
|
346
|
+
- Application.Read.All
|
|
347
|
+
- optionally Directory.Read.All for extended directory/owner checks
|
|
348
|
+
- The signed-in user still needs a suitable Entra directory role.
|
|
349
|
+
|
|
350
|
+
For automation modes, create one daemon app registration:
|
|
351
|
+
|
|
352
|
+
**AzureAppRegistrationSecretMonitor.Daemon**
|
|
353
|
+
- Used by: client-secret, certificate
|
|
354
|
+
- Redirect URI: none
|
|
355
|
+
- Allow public client flows: No
|
|
356
|
+
- Credential:
|
|
357
|
+
- client-secret mode: client secret
|
|
358
|
+
- certificate mode: uploaded certificate
|
|
359
|
+
- Microsoft Graph application permissions:
|
|
360
|
+
- Application.Read.All for read-only monitoring
|
|
361
|
+
- Application.ReadWrite.OwnedBy or Application.ReadWrite.All for secret rotation
|
|
362
|
+
- Admin consent is required.
|
|
363
|
+
---
|
|
364
|
+
|
|
365
|
+
### Modes A: `device-code` and `username-password` — delegated (interactive user)
|
|
366
|
+
|
|
367
|
+
Both modes sign in as *you*, the user. **One App Registration covers both** — you can switch between modes without recreating the registration.
|
|
368
|
+
|
|
369
|
+
> ⚠ **`username-password` only:** does not work if your account has MFA enabled, is a federated account (ADFS, Google etc.) or is a personal Microsoft account (@outlook.com). Use `device-code` instead in those cases.
|
|
370
|
+
|
|
371
|
+
#### 1. Create the App Registration
|
|
372
|
+
|
|
373
|
+
- **Azure Portal → Entra ID → App registrations → New registration**
|
|
374
|
+
- **Name:** `aarm` (any name)
|
|
375
|
+
- **Supported account types:** *Accounts in this organizational directory only*
|
|
376
|
+
- **Redirect URI:** leave empty
|
|
377
|
+
- Click **Register** — note the **Application (client) ID** shown on the Overview page
|
|
378
|
+
|
|
379
|
+
#### 2. Enable public client flows
|
|
380
|
+
|
|
381
|
+
- Go to **Authentication** tab
|
|
382
|
+
- Under *Advanced settings* → **Allow public client flows** → toggle **Yes**
|
|
383
|
+
- Click **Save**
|
|
384
|
+
|
|
385
|
+
#### 3. Add Graph permissions (delegated)
|
|
386
|
+
|
|
387
|
+
- Go to **API permissions** tab
|
|
388
|
+
- **Add a permission → Microsoft Graph → Delegated permissions**
|
|
389
|
+
- Select:
|
|
390
|
+
- `Application.Read.All` — required to list all App Registrations and their secrets
|
|
391
|
+
- `Directory.Read.All` — optional, needed to resolve application owners
|
|
392
|
+
- Click **Grant admin consent for [your org]** — requires a Global Administrator
|
|
393
|
+
|
|
394
|
+
#### 4. Assign a directory role to your user account
|
|
395
|
+
|
|
396
|
+
- Go to **Entra ID → Roles and administrators**
|
|
397
|
+
- Find and open **Cloud Application Administrator** (or **Application Administrator**)
|
|
398
|
+
- Click **Add assignments** → select your user
|
|
399
|
+
- Without this role, delegated `Application.Read.All` only shows apps you personally own
|
|
400
|
+
|
|
401
|
+
#### 5a. Add tenant — `device-code` (MFA supported ✅)
|
|
402
|
+
|
|
403
|
+
```powershell
|
|
404
|
+
aarm tenants add `
|
|
405
|
+
--tenant-id "<tenant-id-guid>" `
|
|
406
|
+
--display-name "Contoso" `
|
|
407
|
+
--auth-mode device-code `
|
|
408
|
+
--client-id "<application-client-id-from-step-1>"
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
First time you run a command, `aarm` prints:
|
|
412
|
+
```
|
|
413
|
+
To sign in, open https://microsoft.com/devicelogin and enter code ABCDE12345
|
|
414
|
+
```
|
|
415
|
+
Open the URL, enter the code, sign in normally (email, password, MFA if required). Token cached afterwards.
|
|
416
|
+
|
|
417
|
+
#### 5b. Add tenant — `username-password` (no MFA ⚠)
|
|
418
|
+
|
|
419
|
+
```powershell
|
|
420
|
+
aarm tenants add `
|
|
421
|
+
--tenant-id "<tenant-id-guid>" `
|
|
422
|
+
--display-name "Contoso" `
|
|
423
|
+
--auth-mode username-password `
|
|
424
|
+
--client-id "<application-client-id-from-step-1>"
|
|
425
|
+
# Prompts: Username (full UPN e.g. you@contoso.com) and Password (hidden)
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Password is stored in **Windows Credential Manager** — never in a plain JSON file.
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
### Mode B: `client-secret` — application permission, no user (unattended)
|
|
433
|
+
|
|
434
|
+
Used for CI/CD pipelines, scheduled jobs, or any scenario without an interactive user. `aarm` acts as a service principal — no directory role assignment needed.
|
|
435
|
+
|
|
436
|
+
#### 1. Create the App Registration
|
|
437
|
+
|
|
438
|
+
- **Azure Portal → Entra ID → App registrations → New registration**
|
|
439
|
+
- **Name:** `aarm-automation` (any name)
|
|
440
|
+
- **Redirect URI:** leave empty
|
|
441
|
+
- Click **Register** — note the **Application (client) ID**
|
|
442
|
+
|
|
443
|
+
#### 2. Add Graph permissions (application, not delegated)
|
|
444
|
+
|
|
445
|
+
- Go to **API permissions** tab
|
|
446
|
+
- **Add a permission → Microsoft Graph → Application permissions**
|
|
447
|
+
- Select:
|
|
448
|
+
- `Application.Read.All` — required
|
|
449
|
+
- `Directory.Read.All` — optional (for owner resolution)
|
|
450
|
+
- Click **Grant admin consent for [your org]** — requires a Global Administrator
|
|
451
|
+
|
|
452
|
+
#### 3. Create a client secret
|
|
453
|
+
|
|
454
|
+
- Go to **Certificates & secrets** tab
|
|
455
|
+
- Click **New client secret** — set an expiry — click **Add**
|
|
456
|
+
- **Copy the secret value immediately** — it is only shown once
|
|
457
|
+
|
|
458
|
+
#### 4. Add tenant
|
|
459
|
+
|
|
460
|
+
```powershell
|
|
461
|
+
aarm tenants add `
|
|
462
|
+
--tenant-id "<tenant-id-guid>" `
|
|
463
|
+
--display-name "Contoso (automation)" `
|
|
464
|
+
--auth-mode client-secret `
|
|
465
|
+
--client-id "<application-client-id-from-step-1>"
|
|
466
|
+
# Prompts: Client Secret (hidden) — paste the value from step 3
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
Secret is stored in Windows Credential Manager.
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
### Mode C: `certificate` — application permission with certificate (high security)
|
|
474
|
+
|
|
475
|
+
Same as Mode B but uses a certificate instead of a client secret. No secret value to rotate manually.
|
|
476
|
+
|
|
477
|
+
**Steps 1–2 are identical to Mode B.**
|
|
478
|
+
|
|
479
|
+
#### 3. Upload a certificate
|
|
480
|
+
|
|
481
|
+
- Go to **Certificates & secrets** tab
|
|
482
|
+
- Click **Upload certificate** — upload your `.cer` or `.pem` (public key)
|
|
483
|
+
- Keep the `.pfx` or `.pem` private key file locally — `aarm` will need the file path
|
|
484
|
+
|
|
485
|
+
#### 4. Add tenant
|
|
486
|
+
|
|
487
|
+
```powershell
|
|
488
|
+
aarm tenants add `
|
|
489
|
+
--tenant-id "<tenant-id-guid>" `
|
|
490
|
+
--display-name "Contoso (cert)" `
|
|
491
|
+
--auth-mode certificate `
|
|
492
|
+
--client-id "<application-client-id-from-step-1>"
|
|
493
|
+
# Prompts: Client ID path (certificate path support in interactive add is planned)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
---
|
|
497
|
+
|
|
498
|
+
### Mode D: `azure-cli` — reuse `az login`, no App Registration needed
|
|
499
|
+
|
|
500
|
+
No App Registration for `aarm` is required. `aarm` delegates to the Azure CLI token cache. The signed-in user still needs **Cloud Application Administrator** or **Application Administrator** to read all apps (same as delegated modes).
|
|
501
|
+
|
|
502
|
+
#### 1. Install the Azure CLI
|
|
503
|
+
|
|
504
|
+
```powershell
|
|
505
|
+
winget install Microsoft.AzureCLI
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
#### 2. Sign in
|
|
509
|
+
|
|
510
|
+
```powershell
|
|
511
|
+
az login
|
|
512
|
+
# A browser opens — sign in with email + password + MFA
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
#### 3. Add tenant (no client-id needed)
|
|
516
|
+
|
|
517
|
+
```powershell
|
|
518
|
+
aarm tenants add `
|
|
519
|
+
--tenant-id "<tenant-id-guid>" `
|
|
520
|
+
--display-name "Contoso" `
|
|
521
|
+
--auth-mode azure-cli
|
|
522
|
+
# No client secret or password stored — uses az token cache
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
### Quick reference — which permissions go where
|
|
528
|
+
|
|
529
|
+
| Mode | Permission tab in Portal | Permission type | `Allow public client flows` |
|
|
530
|
+
|---|---|---|---|
|
|
531
|
+
| `device-code` | API permissions | **Delegated** | **Yes** |
|
|
532
|
+
| `username-password` | API permissions | **Delegated** | **Yes** |
|
|
533
|
+
| `client-secret` | API permissions | **Application** | Not needed |
|
|
534
|
+
| `certificate` | API permissions | **Application** | Not needed |
|
|
535
|
+
| `azure-cli` | — (no App Registration) | — | — |
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
### Reconfiguring a tenant's auth mode
|
|
540
|
+
|
|
541
|
+
```powershell
|
|
542
|
+
aarm tenants remove "Contoso"
|
|
543
|
+
aarm tenants add # choose the new mode interactively
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Setup: Adding your first tenant
|
|
549
|
+
|
|
550
|
+
Every `aarm` command operates against a named tenant. Add a tenant once and all subsequent commands can reference it by name.
|
|
551
|
+
|
|
552
|
+
### Option A — interactive setup
|
|
553
|
+
|
|
554
|
+
```bash
|
|
555
|
+
aarm tenants add
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
The command prompts you for all required values:
|
|
559
|
+
|
|
560
|
+
```
|
|
561
|
+
Tenant ID (GUID): xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
562
|
+
Display name: Contoso PROD
|
|
563
|
+
Auth mode (client-secret/device-code/interactive-browser/certificate/azure-cli): client-secret
|
|
564
|
+
Client ID (App Registration GUID): xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
565
|
+
Client Secret (hidden): ****
|
|
566
|
+
Log Analytics Workspace ID (optional, press Enter to skip):
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
The client secret is stored in **Windows Credential Manager** via the OS credential store. It is never written to a plain JSON file.
|
|
570
|
+
|
|
571
|
+
### Option B — with flags
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
aarm tenants add \
|
|
575
|
+
--tenant-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
|
|
576
|
+
--display-name "Contoso PROD" \
|
|
577
|
+
--auth-mode client-secret \
|
|
578
|
+
--client-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
579
|
+
# aarm then prompts for the client secret
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Non-secret auth modes (no credential stored)
|
|
583
|
+
|
|
584
|
+
```bash
|
|
585
|
+
aarm tenants add \
|
|
586
|
+
--tenant-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" \
|
|
587
|
+
--display-name "Contoso DEV" \
|
|
588
|
+
--auth-mode device-code \
|
|
589
|
+
--client-id "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
With `device-code` or `interactive-browser`, no client secret is stored. Authentication is triggered interactively the first time a command runs.
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## Global options
|
|
597
|
+
|
|
598
|
+
These options apply to all commands:
|
|
599
|
+
|
|
600
|
+
| Option | Description | Default |
|
|
601
|
+
|---|---|---|
|
|
602
|
+
| `--tenant <name-or-id>` | Tenant display name or tenant ID to operate on | — |
|
|
603
|
+
| `--config-dir <path>` | Override the config directory | `~/.aarm` |
|
|
604
|
+
| `--output <format>` | Output format: `table`, `json` | `table` |
|
|
605
|
+
| `--verbose` | Enable verbose/debug output | `false` |
|
|
606
|
+
| `--no-color` | Disable colour in terminal output | `false` |
|
|
607
|
+
| `--version` | Print version | — |
|
|
608
|
+
| `--help` | Show help for any command | — |
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## Command reference
|
|
613
|
+
|
|
614
|
+
### `aarm tenants`
|
|
615
|
+
|
|
616
|
+
Manage the list of configured tenants.
|
|
617
|
+
|
|
618
|
+
#### `aarm tenants list`
|
|
619
|
+
|
|
620
|
+
List all configured tenants.
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
aarm tenants list
|
|
624
|
+
aarm tenants list --output json
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
Output (table):
|
|
628
|
+
```
|
|
629
|
+
┌─────────────────┬──────────────────────────────────────┬───────────────────┬────────────┬────────────┐
|
|
630
|
+
│ Name │ Tenant ID │ Auth Mode │ Client ID │ Last Scan │
|
|
631
|
+
├─────────────────┼──────────────────────────────────────┼───────────────────┼────────────┼────────────┤
|
|
632
|
+
│ Contoso PROD │ xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx │ client-secret │ xxxxxxxx… │ 01/05/2026 │
|
|
633
|
+
│ Contoso DEV │ yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy │ device-code │ yyyyyyyy… │ Never │
|
|
634
|
+
└─────────────────┴──────────────────────────────────────┴───────────────────┴────────────┴────────────┘
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
#### `aarm tenants add`
|
|
638
|
+
|
|
639
|
+
Add or update a tenant. Runs interactively when flags are omitted.
|
|
640
|
+
|
|
641
|
+
```bash
|
|
642
|
+
aarm tenants add [options]
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
| Option | Description |
|
|
646
|
+
|---|---|
|
|
647
|
+
| `--tenant-id <id>` | Entra tenant ID (GUID) |
|
|
648
|
+
| `--display-name <name>` | Friendly label for this tenant |
|
|
649
|
+
| `--auth-mode <mode>` | `client-secret`, `device-code`, `interactive-browser`, `certificate`, `azure-cli`, `username-password` |
|
|
650
|
+
| `--client-id <id>` | App Registration client ID (not needed for `azure-cli`) |
|
|
651
|
+
| `--username <email>` | UPN / email address — only for `username-password` mode |
|
|
652
|
+
| `--workspace-id <id>` | Log Analytics workspace ID (optional) |
|
|
653
|
+
|
|
654
|
+
#### `aarm tenants remove <name-or-id>`
|
|
655
|
+
|
|
656
|
+
Remove a configured tenant.
|
|
657
|
+
|
|
658
|
+
```bash
|
|
659
|
+
aarm tenants remove "Contoso DEV"
|
|
660
|
+
aarm tenants remove "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
### `aarm preflight`
|
|
666
|
+
|
|
667
|
+
Run capability checks and display what the current tenant configuration can do.
|
|
668
|
+
|
|
669
|
+
#### `aarm preflight run`
|
|
670
|
+
|
|
671
|
+
Authenticate, reach Microsoft Graph, and check each permission individually. Reports which capabilities are available and which permissions are missing.
|
|
672
|
+
|
|
673
|
+
```bash
|
|
674
|
+
aarm --tenant "Contoso PROD" preflight run
|
|
675
|
+
aarm --tenant "Contoso PROD" preflight run --output json
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
Output (table):
|
|
679
|
+
```
|
|
680
|
+
Preflight — xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
|
|
681
|
+
Environment : prod
|
|
682
|
+
Checked at : 2026-05-01T12:00:00.000Z
|
|
683
|
+
|
|
684
|
+
Authentication : OK
|
|
685
|
+
Graph reachable : OK
|
|
686
|
+
|
|
687
|
+
Capabilities
|
|
688
|
+
┌────────────────────────────────────┬──────────────────────────────┐
|
|
689
|
+
│ Capability │ Status │
|
|
690
|
+
├────────────────────────────────────┼──────────────────────────────┤
|
|
691
|
+
│ canReadApplications │ [✓] Available │
|
|
692
|
+
│ canReadApplicationSecrets │ [✓] Available │
|
|
693
|
+
│ canReadServicePrincipals │ [✓] Available │
|
|
694
|
+
│ canReadOwners │ [ ] Unavailable │
|
|
695
|
+
│ canReadDirectory │ [ ] Unavailable │
|
|
696
|
+
│ canQueryLogAnalytics │ [ ] Unavailable │
|
|
697
|
+
...
|
|
698
|
+
|
|
699
|
+
Missing permissions
|
|
700
|
+
✗ Delegated permission missing: Directory.Read.All (admin consent required)
|
|
701
|
+
✗ Azure RBAC missing: assign Log Analytics Reader to the signed-in user on the workspace
|
|
702
|
+
|
|
703
|
+
Warnings
|
|
704
|
+
! No Log Analytics workspace configured for this environment.
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
JSON output follows the standard result envelope:
|
|
708
|
+
|
|
709
|
+
```json
|
|
710
|
+
{
|
|
711
|
+
"success": true,
|
|
712
|
+
"metadata": { "tenantId": "...", "environmentName": "prod", ... },
|
|
713
|
+
"data": {
|
|
714
|
+
"authValid": true,
|
|
715
|
+
"graphReachable": true,
|
|
716
|
+
"capabilities": {
|
|
717
|
+
"canReadApplications": true,
|
|
718
|
+
"canReadApplicationSecrets": true,
|
|
719
|
+
"canReadOwners": false,
|
|
720
|
+
...
|
|
721
|
+
},
|
|
722
|
+
"missingPermissions": ["Microsoft Graph: Directory.Read.All"],
|
|
723
|
+
"warnings": [],
|
|
724
|
+
"errors": []
|
|
725
|
+
},
|
|
726
|
+
"warnings": [],
|
|
727
|
+
"errors": []
|
|
728
|
+
}
|
|
729
|
+
```
|
|
730
|
+
|
|
731
|
+
#### `aarm preflight show`
|
|
732
|
+
|
|
733
|
+
Show the last cached preflight result (available after Phase 5 — local history).
|
|
734
|
+
|
|
735
|
+
```bash
|
|
736
|
+
aarm --tenant "Contoso PROD" preflight show
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
#### `aarm preflight explain`
|
|
740
|
+
|
|
741
|
+
Print a human-readable list of every permission `aarm` may need. Output is mode-aware: pass `--tenant` to see only the hints relevant to your configured auth mode.
|
|
742
|
+
|
|
743
|
+
```bash
|
|
744
|
+
# Show hints for all modes (both application and delegated)
|
|
745
|
+
aarm preflight explain
|
|
746
|
+
|
|
747
|
+
# Show only the hints relevant to the auth mode of a specific tenant
|
|
748
|
+
aarm --tenant "Contoso PROD" preflight explain
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
Output with `--tenant` (delegated mode example):
|
|
752
|
+
```
|
|
753
|
+
Delegated modes (device-code · username-password · interactive-browser · azure-cli)
|
|
754
|
+
────────────────────────────────────────────────────────────────────────
|
|
755
|
+
|
|
756
|
+
canReadApplications [admin consent] [user role]
|
|
757
|
+
Delegated permission missing or user role missing:
|
|
758
|
+
API permissions → Microsoft Graph → Delegated → Application.Read.All (admin consent required)
|
|
759
|
+
AND signed-in user must have Cloud Application Administrator or Application Administrator role
|
|
760
|
+
|
|
761
|
+
canReadOwners [admin consent]
|
|
762
|
+
Delegated permission missing: Directory.Read.All (admin consent required)
|
|
763
|
+
|
|
764
|
+
canQueryLogAnalytics
|
|
765
|
+
Azure RBAC missing: assign Log Analytics Reader to the signed-in user on the workspace
|
|
766
|
+
|
|
767
|
+
canCreateApplicationSecrets [post-MVP] [admin consent] [user role]
|
|
768
|
+
Delegated permission missing or user role missing:
|
|
769
|
+
Application.ReadWrite.All (admin consent required) AND Application Administrator role
|
|
770
|
+
...
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
### `aarm apps`
|
|
776
|
+
|
|
777
|
+
Query App Registrations.
|
|
778
|
+
|
|
779
|
+
#### `aarm apps list`
|
|
780
|
+
|
|
781
|
+
List all App Registrations in the tenant with a risk summary.
|
|
782
|
+
|
|
783
|
+
```bash
|
|
784
|
+
aarm --tenant "Contoso PROD" apps list
|
|
785
|
+
aarm --tenant "Contoso PROD" apps list --include-owners
|
|
786
|
+
aarm --tenant "Contoso PROD" apps list --output json
|
|
787
|
+
```
|
|
788
|
+
|
|
789
|
+
| Option | Description |
|
|
790
|
+
|---|---|
|
|
791
|
+
| `--include-owners` | Resolve owners per app. Requires `Directory.Read.All`. |
|
|
792
|
+
|
|
793
|
+
Output (table):
|
|
794
|
+
```
|
|
795
|
+
┌──────────┬────────────────────┬──────────────┬─────────┬─────────┬──────────┐
|
|
796
|
+
│ Risk │ App Name │ Client ID │ Secrets │ Expired │ Expiring │
|
|
797
|
+
├──────────┼────────────────────┼──────────────┼─────────┼─────────┼──────────┤
|
|
798
|
+
│ CRITICAL │ Export Worker │ xxxxxxxx… │ 2 │ 1 │ 0 │
|
|
799
|
+
│ HIGH │ CRM Connector │ yyyyyyyy… │ 1 │ 0 │ 1 │
|
|
800
|
+
│ MEDIUM │ Teams Bot │ zzzzzzzz… │ 1 │ 0 │ 0 │
|
|
801
|
+
│ INFO │ Test Tool │ aaaaaaaa… │ 1 │ 0 │ 0 │
|
|
802
|
+
└──────────┴────────────────────┴──────────────┴─────────┴─────────┴──────────┘
|
|
803
|
+
142 app registration(s)
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
---
|
|
807
|
+
|
|
808
|
+
### `aarm secrets`
|
|
809
|
+
|
|
810
|
+
Query client secrets across all App Registrations.
|
|
811
|
+
|
|
812
|
+
#### `aarm secrets list`
|
|
813
|
+
|
|
814
|
+
List all secrets in the tenant.
|
|
815
|
+
|
|
816
|
+
```bash
|
|
817
|
+
aarm --tenant "Contoso PROD" secrets list
|
|
818
|
+
aarm --tenant "Contoso PROD" secrets list --output json
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
Output (table):
|
|
822
|
+
```
|
|
823
|
+
┌──────────┬──────────────────┬─────────────┬────────────┬──────┬─────────────┐
|
|
824
|
+
│ Risk │ App │ Secret │ Expires │ Days │ Status │
|
|
825
|
+
├──────────┼──────────────────┼─────────────┼────────────┼──────┼─────────────┤
|
|
826
|
+
│ CRITICAL │ Export Worker │ prod-secret │ expired │ -4 │ Expired │
|
|
827
|
+
│ HIGH │ CRM Connector │ crm-prod │ 21/05/2026 │ 21 │ ExpiringSoon│
|
|
828
|
+
│ MEDIUM │ Teams Bot │ bot-secret │ 10/07/2026 │ 71 │ Valid │
|
|
829
|
+
│ INFO │ Test Tool │ dev-secret │ 01/11/2026 │ 185 │ Valid │
|
|
830
|
+
└──────────┴──────────────────┴─────────────┴────────────┴──────┴─────────────┘
|
|
831
|
+
4 secret(s)
|
|
832
|
+
```
|
|
833
|
+
|
|
834
|
+
#### `aarm secrets expiring`
|
|
835
|
+
|
|
836
|
+
List only secrets expiring within a given window.
|
|
837
|
+
|
|
838
|
+
```bash
|
|
839
|
+
aarm --tenant "Contoso PROD" secrets expiring
|
|
840
|
+
aarm --tenant "Contoso PROD" secrets expiring --days 30
|
|
841
|
+
aarm --tenant "Contoso PROD" secrets expiring --months 3
|
|
842
|
+
aarm --tenant "Contoso PROD" secrets expiring --days 90 --output json
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
| Option | Default | Description |
|
|
846
|
+
|---|---|---|
|
|
847
|
+
| `--days <n>` | `30` | Show secrets expiring within `n` days |
|
|
848
|
+
| `--months <n>` | — | Show secrets expiring within `n` months (converted to `n × 30` days) |
|
|
849
|
+
|
|
850
|
+
#### `aarm secrets expired`
|
|
851
|
+
|
|
852
|
+
List only secrets that are already expired.
|
|
853
|
+
|
|
854
|
+
```bash
|
|
855
|
+
aarm --tenant "Contoso PROD" secrets expired
|
|
856
|
+
aarm --tenant "Contoso PROD" secrets expired --output json
|
|
857
|
+
```
|
|
858
|
+
|
|
859
|
+
---
|
|
860
|
+
|
|
861
|
+
### `aarm usage`
|
|
862
|
+
|
|
863
|
+
Analyze secret usage via Azure Monitor / Log Analytics. Requires a Log Analytics workspace with
|
|
864
|
+
`AADServicePrincipalSignInLogs` enabled. Configure the workspace ID when adding a tenant with
|
|
865
|
+
`aarm tenants add --workspace-id <id>`.
|
|
866
|
+
|
|
867
|
+
All subcommands accept `--days <n>` (default 90) to set the look-back window.
|
|
868
|
+
|
|
869
|
+
#### `aarm usage analyze`
|
|
870
|
+
|
|
871
|
+
Show overall sign-in activity for an App Registration over the look-back period.
|
|
872
|
+
|
|
873
|
+
```bash
|
|
874
|
+
aarm --tenant "Contoso PROD" usage analyze --app-id "<client-id>"
|
|
875
|
+
aarm --tenant "Contoso PROD" usage analyze --app-name "CRM Connector"
|
|
876
|
+
aarm --tenant "Contoso PROD" usage analyze --app-name "CRM Connector" --days 30
|
|
877
|
+
aarm --tenant "Contoso PROD" usage analyze --app-id "<client-id>" --output json
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
| Option | Description |
|
|
881
|
+
|---|---|
|
|
882
|
+
| `--app-id <id>` | App Registration client ID (one of `--app-id` or `--app-name` is required) |
|
|
883
|
+
| `--app-name <name>` | App Registration display name (resolved via Graph) |
|
|
884
|
+
| `--days <n>` | Look-back window in days (default: 90) |
|
|
885
|
+
|
|
886
|
+
Output shows total, successful, and failed sign-in counts, broken down by service principal and source IP.
|
|
887
|
+
|
|
888
|
+
#### `aarm usage analyze-secret`
|
|
889
|
+
|
|
890
|
+
Show activity broken down per secret key ID — useful to identify which specific credential is used.
|
|
891
|
+
|
|
892
|
+
```bash
|
|
893
|
+
aarm --tenant "Contoso PROD" usage analyze-secret --app-id "<client-id>"
|
|
894
|
+
aarm --tenant "Contoso PROD" usage analyze-secret --app-name "CRM Connector" --days 14
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
#### `aarm usage last-seen`
|
|
898
|
+
|
|
899
|
+
Show when each secret key ID was last used (last successful sign-in timestamp).
|
|
900
|
+
|
|
901
|
+
```bash
|
|
902
|
+
aarm --tenant "Contoso PROD" usage last-seen --app-id "<client-id>"
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
Output includes `lastSeenAt` per key ID, making it easy to identify credentials that are still actively used before rotating them.
|
|
906
|
+
|
|
907
|
+
#### `aarm usage rotation-check`
|
|
908
|
+
|
|
909
|
+
After rotating a secret, verify that the old key ID has stopped appearing in sign-in logs.
|
|
910
|
+
|
|
911
|
+
```bash
|
|
912
|
+
aarm --tenant "Contoso PROD" usage rotation-check --app-id "<client-id>" --days 7
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
Returns non-zero if the old credential is still seen within the look-back window.
|
|
916
|
+
|
|
917
|
+
---
|
|
918
|
+
|
|
919
|
+
### `aarm report`
|
|
920
|
+
|
|
921
|
+
Generate reports from the current secret inventory. Report commands perform a full inventory scan and then format the output for human consumption or automation.
|
|
922
|
+
|
|
923
|
+
All subcommands accept `--output <format>` with values `table` (default), `json`, `markdown`, or `csv`.
|
|
924
|
+
|
|
925
|
+
#### `aarm report expiring`
|
|
926
|
+
|
|
927
|
+
Report all secrets expiring within a configurable window, sorted by days remaining.
|
|
928
|
+
|
|
929
|
+
```bash
|
|
930
|
+
aarm --tenant "Contoso PROD" report expiring
|
|
931
|
+
aarm --tenant "Contoso PROD" report expiring --days 30
|
|
932
|
+
aarm --tenant "Contoso PROD" report expiring --output markdown
|
|
933
|
+
aarm --tenant "Contoso PROD" report expiring --output csv
|
|
934
|
+
```
|
|
935
|
+
|
|
936
|
+
| Option | Default | Description |
|
|
937
|
+
|---|---|---|
|
|
938
|
+
| `--days <n>` | `30` | Report secrets expiring within `n` days |
|
|
939
|
+
|
|
940
|
+
#### `aarm report tenant-summary`
|
|
941
|
+
|
|
942
|
+
High-level summary of the entire tenant: total apps, total secrets, risk distribution.
|
|
943
|
+
|
|
944
|
+
```bash
|
|
945
|
+
aarm --tenant "Contoso PROD" report tenant-summary
|
|
946
|
+
aarm --tenant "Contoso PROD" report tenant-summary --output json
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
#### `aarm report findings`
|
|
950
|
+
|
|
951
|
+
Report all secrets at or above a minimum risk level.
|
|
952
|
+
|
|
953
|
+
```bash
|
|
954
|
+
aarm --tenant "Contoso PROD" report findings
|
|
955
|
+
aarm --tenant "Contoso PROD" report findings --min-risk high
|
|
956
|
+
aarm --tenant "Contoso PROD" report findings --min-risk critical --output json
|
|
957
|
+
```
|
|
958
|
+
|
|
959
|
+
| Option | Default | Description |
|
|
960
|
+
|---|---|---|
|
|
961
|
+
| `--min-risk <level>` | `medium` | Minimum risk level: `info`, `low`, `medium`, `high`, `critical` |
|
|
962
|
+
|
|
963
|
+
#### `aarm report rotation-guide`
|
|
964
|
+
|
|
965
|
+
Produce a rotation checklist for all secrets expiring within the look-back window.
|
|
966
|
+
|
|
967
|
+
```bash
|
|
968
|
+
aarm --tenant "Contoso PROD" report rotation-guide
|
|
969
|
+
aarm --tenant "Contoso PROD" report rotation-guide --days 14 --output markdown
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
---
|
|
973
|
+
|
|
974
|
+
## Output formats
|
|
975
|
+
|
|
976
|
+
### Table (default, human-readable)
|
|
977
|
+
|
|
978
|
+
```bash
|
|
979
|
+
aarm --tenant "Contoso PROD" secrets expiring --days 30
|
|
980
|
+
```
|
|
981
|
+
|
|
982
|
+
Colour-coded by risk: red = Critical/High, yellow = Medium, cyan = Low, dim = Info.
|
|
983
|
+
|
|
984
|
+
Disable colours with `--no-color` for use in terminals that do not support ANSI codes.
|
|
985
|
+
|
|
986
|
+
### JSON (for automation and scripts)
|
|
987
|
+
|
|
988
|
+
```bash
|
|
989
|
+
aarm --tenant "Contoso PROD" secrets expiring --days 30 --output json
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
All JSON output follows the standard result envelope:
|
|
993
|
+
|
|
994
|
+
```json
|
|
995
|
+
{
|
|
996
|
+
"success": true,
|
|
997
|
+
"metadata": {
|
|
998
|
+
"tenantId": "<tenant-id>",
|
|
999
|
+
"environmentName": "default",
|
|
1000
|
+
"generatedAt": "2026-05-01T12:00:00.000Z",
|
|
1001
|
+
"toolVersion": "0.1.0"
|
|
1002
|
+
},
|
|
1003
|
+
"data": [
|
|
1004
|
+
{
|
|
1005
|
+
"applicationObjectId": "...",
|
|
1006
|
+
"appId": "...",
|
|
1007
|
+
"appDisplayName": "CRM Connector",
|
|
1008
|
+
"keyId": "...",
|
|
1009
|
+
"displayName": "crm-prod",
|
|
1010
|
+
"hint": "abc",
|
|
1011
|
+
"startDateTime": "2025-01-15T00:00:00Z",
|
|
1012
|
+
"endDateTime": "2026-05-21T00:00:00Z",
|
|
1013
|
+
"daysUntilExpiry": 21,
|
|
1014
|
+
"status": "ExpiringSoon",
|
|
1015
|
+
"riskLevel": "High"
|
|
1016
|
+
}
|
|
1017
|
+
],
|
|
1018
|
+
"warnings": [],
|
|
1019
|
+
"errors": []
|
|
1020
|
+
}
|
|
1021
|
+
```
|
|
1022
|
+
|
|
1023
|
+
When using JSON output:
|
|
1024
|
+
- No spinner, colour codes, or decorative text
|
|
1025
|
+
- Non-zero exit only for real command failures (not for findings)
|
|
1026
|
+
- Warnings and errors are inside the envelope, not on stderr
|
|
1027
|
+
|
|
1028
|
+
---
|
|
1029
|
+
|
|
1030
|
+
## Scenarios
|
|
1031
|
+
|
|
1032
|
+
### Scenario 1: First-time setup
|
|
1033
|
+
|
|
1034
|
+
```bash
|
|
1035
|
+
# Install
|
|
1036
|
+
npm install -g @brunsforge/aarm
|
|
1037
|
+
|
|
1038
|
+
# Add your tenant (interactive)
|
|
1039
|
+
aarm tenants add
|
|
1040
|
+
|
|
1041
|
+
# Check what permissions you have
|
|
1042
|
+
aarm --tenant "Contoso PROD" preflight run
|
|
1043
|
+
|
|
1044
|
+
# Run a quick secret audit
|
|
1045
|
+
aarm --tenant "Contoso PROD" secrets list
|
|
1046
|
+
```
|
|
1047
|
+
|
|
1048
|
+
### Scenario 2: Daily monitoring check
|
|
1049
|
+
|
|
1050
|
+
```bash
|
|
1051
|
+
# See what's expiring in the next 90 days
|
|
1052
|
+
aarm --tenant "Contoso PROD" secrets expiring --months 3
|
|
1053
|
+
|
|
1054
|
+
# Find anything already expired
|
|
1055
|
+
aarm --tenant "Contoso PROD" secrets expired
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
### Scenario 3: CI/CD pipeline integration
|
|
1059
|
+
|
|
1060
|
+
Use `--output json` and check the exit code:
|
|
1061
|
+
|
|
1062
|
+
```yaml
|
|
1063
|
+
# GitHub Actions example
|
|
1064
|
+
- name: Check for expired secrets
|
|
1065
|
+
run: |
|
|
1066
|
+
aarm --tenant "Contoso PROD" --output json secrets expired \
|
|
1067
|
+
| jq '.data | length' \
|
|
1068
|
+
| xargs -I{} test {} -eq 0
|
|
1069
|
+
env:
|
|
1070
|
+
AARM_CONFIG_DIR: ${{ runner.temp }}/aarm-config
|
|
1071
|
+
```
|
|
1072
|
+
|
|
1073
|
+
Or use the exit code for threshold alerting (exit 10 when findings exceed threshold — planned for a future release).
|
|
1074
|
+
|
|
1075
|
+
### Scenario 4: Pre-rotation check
|
|
1076
|
+
|
|
1077
|
+
Before rotating a secret, check if the old key is still actively used:
|
|
1078
|
+
|
|
1079
|
+
```bash
|
|
1080
|
+
# Check which apps have secrets expiring within 14 days
|
|
1081
|
+
aarm --tenant "Contoso PROD" secrets expiring --days 14 --output json \
|
|
1082
|
+
| jq '.data[] | {app: .appDisplayName, keyId: .keyId, days: .daysUntilExpiry}'
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1085
|
+
After rotation, you can verify the old key is no longer used with the `usage rotation-check` command (Phase 6).
|
|
1086
|
+
|
|
1087
|
+
### Scenario 5: Verify permissions before onboarding a new tenant
|
|
1088
|
+
|
|
1089
|
+
```bash
|
|
1090
|
+
# After registering the app and granting admin consent:
|
|
1091
|
+
aarm tenants add --tenant-id "<new-tenant>" --display-name "New Customer" \
|
|
1092
|
+
--auth-mode client-secret --client-id "<client-id>"
|
|
1093
|
+
|
|
1094
|
+
# Immediately run preflight to confirm what's available
|
|
1095
|
+
aarm --tenant "New Customer" preflight run
|
|
1096
|
+
aarm preflight explain
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
---
|
|
1100
|
+
|
|
1101
|
+
## Exit codes
|
|
1102
|
+
|
|
1103
|
+
| Code | Meaning |
|
|
1104
|
+
|---|---|
|
|
1105
|
+
| `0` | Success |
|
|
1106
|
+
| `1` | General error |
|
|
1107
|
+
| `2` | Authentication failed (token acquisition error) |
|
|
1108
|
+
| `3` | Missing permission or capability |
|
|
1109
|
+
| `4` | Configuration invalid (tenant not found, missing client ID, etc.) |
|
|
1110
|
+
| `5` | No data source available (e.g. Log Analytics not configured) |
|
|
1111
|
+
| `10` | Findings found above configured threshold (CI threshold mode — future release) |
|
|
1112
|
+
|
|
1113
|
+
---
|
|
1114
|
+
|
|
1115
|
+
## Configuration directory
|
|
1116
|
+
|
|
1117
|
+
By default, `aarm` stores all files under `~/.aarm/`:
|
|
1118
|
+
|
|
1119
|
+
| Platform | Path |
|
|
1120
|
+
|---|---|
|
|
1121
|
+
| Windows | `C:\Users\<you>\.aarm\` |
|
|
1122
|
+
| macOS | `/Users/<you>/.aarm/` |
|
|
1123
|
+
| Linux | `/home/<you>/.aarm/` |
|
|
1124
|
+
|
|
1125
|
+
### Directory layout
|
|
1126
|
+
|
|
1127
|
+
```
|
|
1128
|
+
~/.aarm/
|
|
1129
|
+
tenants.json ← tenant profiles (non-sensitive)
|
|
1130
|
+
shared with the AARM MAUI desktop app
|
|
1131
|
+
|
|
1132
|
+
history/
|
|
1133
|
+
{tenantId}/
|
|
1134
|
+
secrets-2026-05-08T06-00-00-000Z.json ← secrets scan result (SecretSummary[])
|
|
1135
|
+
preflight-2026-05-08T06-00-00-000Z.json ← preflight result (PreflightResult)
|
|
1136
|
+
```
|
|
1137
|
+
|
|
1138
|
+
One folder per tenant — no additional sub-levels. An Azure AD tenant has exactly one Graph API
|
|
1139
|
+
and therefore one set of App Registrations and secrets.
|
|
1140
|
+
|
|
1141
|
+
**Retention:** up to **50 files per type per tenant**. Older files are pruned automatically after each scan.
|
|
1142
|
+
|
|
1143
|
+
**File naming:** ISO 8601 timestamp with `:` and `.` replaced by `-` so filenames are valid on Windows.
|
|
1144
|
+
|
|
1145
|
+
### What is NOT stored here
|
|
1146
|
+
|
|
1147
|
+
Client secrets, user passwords, and the MAUI Cloud Mode function key are **never written to JSON files**. They are stored in the **OS credential store**:
|
|
1148
|
+
|
|
1149
|
+
| Platform | Credential store |
|
|
1150
|
+
|---|---|
|
|
1151
|
+
| Windows | Windows Credential Manager (`advapi32.dll` — same store as the MAUI app via P/Invoke) |
|
|
1152
|
+
| macOS | macOS Keychain |
|
|
1153
|
+
| Linux | libsecret / GNOME Keyring |
|
|
1154
|
+
|
|
1155
|
+
The MAUI app uses the exact same credential store with a compatible key format, so credentials configured via `aarm tenants add` are immediately available to the desktop app and vice versa.
|
|
1156
|
+
|
|
1157
|
+
### Override the config directory
|
|
1158
|
+
|
|
1159
|
+
Use `--config-dir` to point to a different directory — useful in CI/CD pipelines or when running multiple isolated configurations:
|
|
1160
|
+
|
|
1161
|
+
```bash
|
|
1162
|
+
# CI/CD: use a pipeline-specific config directory
|
|
1163
|
+
aarm --config-dir /tmp/aarm-ci --tenant "Contoso PROD" secrets list
|
|
1164
|
+
|
|
1165
|
+
# Multiple environments side by side
|
|
1166
|
+
aarm --config-dir ~/.aarm-staging --tenant "Contoso Staging" secrets list
|
|
1167
|
+
|
|
1168
|
+
# Or set the environment variable (applies to all commands in the shell session)
|
|
1169
|
+
export AARM_CONFIG_DIR=/opt/aarm-config
|
|
1170
|
+
aarm --tenant "Contoso PROD" secrets list
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
1175
|
+
## Related
|
|
1176
|
+
|
|
1177
|
+
| Resource | Description |
|
|
1178
|
+
|---|---|
|
|
1179
|
+
| [`@brunsforge/azure-app-registration-monitor`](../core/README.md) | TypeScript library used as the engine |
|
|
1180
|
+
| [Azure App Registration Monitor (MAUI)](../../apps/maui-blazor/README.md) | Desktop UI for the same functionality |
|