@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.
Files changed (43) hide show
  1. package/README.md +1180 -0
  2. package/dist/aarm.cjs +39260 -0
  3. package/dist/aarm.mjs +39262 -0
  4. package/dist/commands/apps.d.ts +2 -0
  5. package/dist/commands/apps.js +32 -0
  6. package/dist/commands/apps.js.map +1 -0
  7. package/dist/commands/preflight.d.ts +2 -0
  8. package/dist/commands/preflight.js +160 -0
  9. package/dist/commands/preflight.js.map +1 -0
  10. package/dist/commands/report.d.ts +2 -0
  11. package/dist/commands/report.js +166 -0
  12. package/dist/commands/report.js.map +1 -0
  13. package/dist/commands/secrets.d.ts +2 -0
  14. package/dist/commands/secrets.js +88 -0
  15. package/dist/commands/secrets.js.map +1 -0
  16. package/dist/commands/tenants.d.ts +2 -0
  17. package/dist/commands/tenants.js +209 -0
  18. package/dist/commands/tenants.js.map +1 -0
  19. package/dist/commands/usage.d.ts +2 -0
  20. package/dist/commands/usage.js +203 -0
  21. package/dist/commands/usage.js.map +1 -0
  22. package/dist/config/ConfigStore.d.ts +12 -0
  23. package/dist/config/ConfigStore.js +53 -0
  24. package/dist/config/ConfigStore.js.map +1 -0
  25. package/dist/config/CredentialStore.d.ts +8 -0
  26. package/dist/config/CredentialStore.js +29 -0
  27. package/dist/config/CredentialStore.js.map +1 -0
  28. package/dist/config/HistoryStore.d.ts +19 -0
  29. package/dist/config/HistoryStore.js +64 -0
  30. package/dist/config/HistoryStore.js.map +1 -0
  31. package/dist/exitCodes.d.ts +9 -0
  32. package/dist/exitCodes.js +10 -0
  33. package/dist/exitCodes.js.map +1 -0
  34. package/dist/index.d.ts +2 -0
  35. package/dist/index.js +38 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/output/formatters.d.ts +10 -0
  38. package/dist/output/formatters.js +129 -0
  39. package/dist/output/formatters.js.map +1 -0
  40. package/dist/shared/context.d.ts +31 -0
  41. package/dist/shared/context.js +124 -0
  42. package/dist/shared/context.js.map +1 -0
  43. 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 |