@knowttl/mg-axi 0.1.1 → 0.1.3

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 (48) hide show
  1. package/README.md +251 -77
  2. package/dist/api.js +253 -8
  3. package/dist/bin/mg-axi-hook.js +31 -0
  4. package/dist/catalogue.js +693 -2
  5. package/dist/cli.js +155 -17
  6. package/dist/docs.js +175 -46
  7. package/dist/doctor.js +2 -2
  8. package/dist/entra-access-reviews.js +11 -4
  9. package/dist/entra-apps.js +171 -14
  10. package/dist/entra-audit-logs.js +101 -16
  11. package/dist/entra-auth-methods.js +8 -4
  12. package/dist/entra-certificate-auth.js +12 -8
  13. package/dist/entra-conditional-access.js +425 -15
  14. package/dist/entra-contacts.js +19 -12
  15. package/dist/entra-contracts.js +7 -4
  16. package/dist/entra-custom-security-attributes.js +19 -12
  17. package/dist/entra-data-policy-operations.js +7 -4
  18. package/dist/entra-delegated-admin.js +55 -6
  19. package/dist/entra-deleted-items.js +6 -4
  20. package/dist/entra-directory-objects.js +6 -4
  21. package/dist/entra-directory.js +19 -12
  22. package/dist/entra-domains.js +13 -8
  23. package/dist/entra-entitlement-management.js +12 -4
  24. package/dist/entra-federation-configurations.js +7 -4
  25. package/dist/entra-grants.js +8 -4
  26. package/dist/entra-group-lifecycle.js +13 -8
  27. package/dist/entra-group-pim.js +431 -0
  28. package/dist/entra-groups.js +13 -8
  29. package/dist/entra-identity-providers.js +7 -4
  30. package/dist/entra-lifecycle-workflows.js +152 -13
  31. package/dist/entra-multi-tenant-organization.js +7 -4
  32. package/dist/entra-on-premises-synchronization.js +6 -4
  33. package/dist/entra-organization.js +13 -8
  34. package/dist/entra-risk-prevention.js +442 -0
  35. package/dist/entra-risk.js +224 -9
  36. package/dist/entra-roles.js +204 -12
  37. package/dist/entra-subscriptions.js +6 -4
  38. package/dist/entra-terms-of-use.js +13 -8
  39. package/dist/entra-users.js +7 -4
  40. package/dist/graph-session.js +30 -5
  41. package/dist/hook.js +84 -0
  42. package/dist/list-totals.js +52 -0
  43. package/dist/setup.js +9 -1
  44. package/docs/coverage.md +743 -610
  45. package/inventory/operations.json +2 -2
  46. package/inventory/schema.json +1 -1
  47. package/package.json +4 -3
  48. package/skills/mg-axi/SKILL.md +86 -1
@@ -1,8 +1,12 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { KNOWN_APP_ROLE_FIELDS } from "./entra-grants.js";
3
+ import { listTotals } from "./list-totals.js";
2
4
  import { SAFE_CREDENTIAL_FIELDS } from "./graph-session.js";
3
5
  // READ-07: the Entra application and service-principal read mapping behind
4
6
  // `mg-axi entra application list/show`, `mg-axi entra service-principal
5
- // list/show` and the two `owner list` relationship reads. Operation
7
+ // list/show`, the two `owner list` relationship reads, the service-principal
8
+ // `app-role-assigned-to` and `app-role-assignment show` assignment reads and
9
+ // the two `federated-credential` collections. Operation
6
10
  // construction stays beside its command; the shared session owns URLs,
7
11
  // credentials, paging, retries and error translation, and the SDK owns TOON
8
12
  // rendering. This module only maps flags to session calls and projects rows
@@ -13,14 +17,18 @@ import { SAFE_CREDENTIAL_FIELDS } from "./graph-session.js";
13
17
  // servicePrincipal-list-owners operation documentation on 2026-10-04.
14
18
  // Delegated and application reads take Application.Read.All; the object id
15
19
  // (`id`) is never conflated with the client ID (`appId`) - both ride in the
16
- // compact list defaults so callers can see the distinction. Consent grants
17
- // (appRoleAssignments, oauth2PermissionGrants) are READ-08 and stay out of
18
- // this slice; only owners are projected as relationships.
20
+ // compact list defaults so callers can see the distinction. Delegated
21
+ // permission grants (oauth2PermissionGrants) are READ-08 and stay out of
22
+ // this slice; only owners, direct assignments and federated credentials are
23
+ // projected as relationships. Alternate-key lookups by appId and
24
+ // federated-credential lookups by name stay out too: they need a validated
25
+ // function-argument binding the session guard does not have, and widening
26
+ // the guard is out of scope for a read slice.
19
27
  //
20
28
  // Credential safety belongs to the shared session's SAFE_CREDENTIAL_FIELDS
21
29
  // projection, applied before collection buffering and again on cursor decode.
22
30
  // Local projection uses that same allowlist for the displayed view.
23
- // The six GET operations below are the only ones this module ever binds;
31
+ // The thirteen GET operations below are the only ones this module ever binds;
24
32
  // secret-minting routes (addPassword, addKey) and every write stay refused before
25
33
  // credentials via checkReadOperation.
26
34
  // Every application property this slice may request or display. Anything
@@ -51,6 +59,20 @@ export const KNOWN_SP_FIELDS = [
51
59
  "passwordCredentials",
52
60
  ];
53
61
  const KNOWN_SPS = new Set(KNOWN_SP_FIELDS);
62
+ const KNOWN_ASSIGNED = new Set(KNOWN_APP_ROLE_FIELDS);
63
+ // Federated-credential rows carry workload-identity metadata only: issuer,
64
+ // subject and audiences name the trusted token source. There is no secret
65
+ // material on this resource, and the projection still drops anything outside
66
+ // this set so a future property can never leak through.
67
+ export const KNOWN_FEDERATED_FIELDS = [
68
+ "id",
69
+ "name",
70
+ "issuer",
71
+ "subject",
72
+ "description",
73
+ "audiences",
74
+ ];
75
+ const KNOWN_FEDERATED = new Set(KNOWN_FEDERATED_FIELDS);
54
76
  // Owner rows are directoryObjects of mixed types; richer per-type fields
55
77
  // need single-object reads, so only these stay selectable.
56
78
  export const KNOWN_OWNER_FIELDS = ["id", "displayName", "mail"];
@@ -67,6 +89,17 @@ const DEFAULT_APP_SHOW_SELECT = [...KNOWN_APP_FIELDS];
67
89
  const DEFAULT_SP_SHOW_SELECT = [...KNOWN_SP_FIELDS];
68
90
  // Compact owner rows: identifier, display name and address.
69
91
  const DEFAULT_OWNER_SELECT = ["id", "displayName", "mail"];
92
+ // Compact federated-credential rows: the credential id, its name and the
93
+ // trusted token source. The full set adds the human description and the
94
+ // audience list.
95
+ const DEFAULT_FEDERATED_LIST_SELECT = ["id", "name", "issuer", "subject"];
96
+ const DEFAULT_FEDERATED_SHOW_SELECT = [...KNOWN_FEDERATED_FIELDS];
97
+ // Compact assignment rows: the assignment id, the granted role and who
98
+ // holds it. principalId is constant for one service principal's
99
+ // appRoleAssignments list but varies across appRoleAssignedTo rows.
100
+ const DEFAULT_ASSIGNED_LIST_SELECT = ["id", "appRoleId", "principalDisplayName", "principalId", "principalType", "resourceDisplayName", "resourceId"];
101
+ ;
102
+ const DEFAULT_ASSIGNED_SHOW_SELECT = [...KNOWN_APP_ROLE_FIELDS];
70
103
  // Application.Read.All covers applications, service principals and owners in
71
104
  // both modes; delegated reads default to it unless --scopes overrides it.
72
105
  export const DEFAULT_DELEGATED_SCOPES = ["https://graph.microsoft.com/Application.Read.All"];
@@ -77,9 +110,16 @@ const READ_OPERATIONS = {
77
110
  "GET:/applications": "application",
78
111
  "GET:/applications/{application-id}": "application",
79
112
  "GET:/applications/{application-id}/owners": "application",
113
+ "GET:/applications/{application-id}/federatedIdentityCredentials": "application",
114
+ "GET:/applications/{application-id}/federatedIdentityCredentials/{federatedIdentityCredential-id}": "application",
80
115
  "GET:/servicePrincipals": "service-principal",
81
116
  "GET:/servicePrincipals/{servicePrincipal-id}": "service-principal",
82
117
  "GET:/servicePrincipals/{servicePrincipal-id}/owners": "service-principal",
118
+ "GET:/servicePrincipals/{servicePrincipal-id}/appRoleAssignedTo": "service-principal",
119
+ "GET:/servicePrincipals/{servicePrincipal-id}/appRoleAssignedTo/{appRoleAssignment-id}": "service-principal",
120
+ "GET:/servicePrincipals/{servicePrincipal-id}/appRoleAssignments/{appRoleAssignment-id}": "service-principal",
121
+ "GET:/servicePrincipals/{servicePrincipal-id}/federatedIdentityCredentials": "service-principal",
122
+ "GET:/servicePrincipals/{servicePrincipal-id}/federatedIdentityCredentials/{federatedIdentityCredential-id}": "service-principal",
83
123
  };
84
124
  function checkReadOperation(operation, help) {
85
125
  // Version stays the session's decision: beta reads remain preview-gated
@@ -87,7 +127,7 @@ function checkReadOperation(operation, help) {
87
127
  // fabricated secret-minting or write route can never reach credentials.
88
128
  const route = `${operation.method}:${operation.path}`;
89
129
  if (operation.method !== "GET" || !Object.hasOwn(READ_OPERATIONS, route)) {
90
- throw new AxiError(`Refused non-read route ${operation.id}: READ-07 serves only the six catalogued application and service-principal GETs`, "VALIDATION_ERROR", [
130
+ throw new AxiError(`Refused non-read route ${operation.id}: READ-07 serves only the thirteen catalogued application and service-principal GETs`, "VALIDATION_ERROR", [
91
131
  help,
92
132
  "Secret-minting routes (addPassword, addKey) and writes are never constructed here; consent grants belong to READ-08",
93
133
  ]);
@@ -270,23 +310,25 @@ async function listCollection(session, flags, profile, operation, help, profileN
270
310
  if (!result.complete) {
271
311
  return {
272
312
  [key]: rows,
273
- count: { returned: rows.length, complete: false, reason: result.reason },
313
+ ...listTotals(rows.length, result.total, emptyNoun, false),
314
+ complete: false,
315
+ reason: result.reason,
274
316
  cursor: result.cursor,
275
317
  help: [...truncationHints, resumeHint(profileName), showHint],
276
318
  };
277
319
  }
278
- const count = { returned: rows.length, complete: true };
279
320
  if (!rows.length) {
280
321
  return {
281
322
  [key]: rows,
282
- count,
323
+ ...listTotals(rows.length, result.total, emptyNoun, true),
324
+ complete: true,
283
325
  help: [
284
326
  `0 ${emptyNoun} matched; the absence of results is the answer, not an error`,
285
327
  showHint,
286
328
  ],
287
329
  };
288
330
  }
289
- return { [key]: rows, count, help: [...truncationHints, showHint] };
331
+ return { [key]: rows, ...listTotals(rows.length, result.total, emptyNoun, true), complete: true, help: [...truncationHints, showHint] };
290
332
  }
291
333
  async function showSingle(session, flags, profile, operation, help, profileName, command, key, known, knownList, defaults, param, flag) {
292
334
  checkReadOperation(operation, help);
@@ -351,20 +393,22 @@ async function listOwners(session, flags, profile, operation, help, profileName,
351
393
  if (!result.complete) {
352
394
  return {
353
395
  owners: rows,
354
- count: { returned: rows.length, complete: false, reason: result.reason },
396
+ ...listTotals(rows.length, result.total, "owners", false),
397
+ complete: false,
398
+ reason: result.reason,
355
399
  cursor: result.cursor,
356
400
  help: [...truncationHints, ...limitedHints, resumeHint(profileName)],
357
401
  };
358
402
  }
359
- const count = { returned: rows.length, complete: true };
360
403
  if (!rows.length) {
361
404
  return {
362
405
  owners: rows,
363
- count,
406
+ ...listTotals(rows.length, result.total, "owners", true),
407
+ complete: true,
364
408
  help: [...limitedHints, "0 owners matched; the absence of results is the answer, not an error"],
365
409
  };
366
410
  }
367
- return { owners: rows, count, help: [...truncationHints, ...limitedHints] };
411
+ return { owners: rows, ...listTotals(rows.length, result.total, "owners", true), complete: true, help: [...truncationHints, ...limitedHints] };
368
412
  }
369
413
  export async function listApplicationOwners(session, flags, profile, operation, help, profileName) {
370
414
  return listOwners(session, flags, profile, operation, help, profileName, "entra application owner list", "application-id", "application");
@@ -372,3 +416,116 @@ export async function listApplicationOwners(session, flags, profile, operation,
372
416
  export async function listServicePrincipalOwners(session, flags, profile, operation, help, profileName) {
373
417
  return listOwners(session, flags, profile, operation, help, profileName, "entra service-principal owner list", "servicePrincipal-id", "service-principal");
374
418
  }
419
+ async function listChildCollection(session, flags, profile, operation, help, profileName, child, known, knownList, defaults) {
420
+ checkReadOperation(operation, help);
421
+ const parent = String(flags[child.parentFlag]);
422
+ if (!parent.trim())
423
+ throw new AxiError(`--${child.parentFlag} needs ${child.parentLabel}`, "VALIDATION_ERROR", [help]);
424
+ const common = collectionCommon(session, flags, known, knownList, defaults, operation, help, profile);
425
+ const args = collectArgs(profile, operation, common, flags, help);
426
+ args.params = { [child.parentParam]: parent };
427
+ const result = await session.collect(args);
428
+ const effectiveFlags = { ...flags, select: result.query.$select ?? defaults.join(",") };
429
+ if (result.query.$filter !== undefined)
430
+ effectiveFlags.filter = result.query.$filter;
431
+ const rows = [];
432
+ let truncated = false;
433
+ for (const row of result.value) {
434
+ const projected = project(row, common.fields, common.full);
435
+ rows.push(projected.row);
436
+ truncated = truncated || projected.truncated;
437
+ }
438
+ const truncationHints = truncated ? [fullHint(child.command, effectiveFlags, profileName)] : [];
439
+ if (!result.complete) {
440
+ return {
441
+ [child.key]: rows,
442
+ ...listTotals(rows.length, result.total, child.emptyNoun, false),
443
+ complete: false,
444
+ reason: result.reason,
445
+ cursor: result.cursor,
446
+ help: [...truncationHints, resumeHint(profileName), child.showHint],
447
+ };
448
+ }
449
+ if (!rows.length) {
450
+ return {
451
+ [child.key]: rows,
452
+ ...listTotals(rows.length, result.total, child.emptyNoun, true),
453
+ complete: true,
454
+ help: [
455
+ `0 ${child.emptyNoun} matched; the absence of results is the answer, not an error`,
456
+ child.showHint,
457
+ ],
458
+ };
459
+ }
460
+ return { [child.key]: rows, ...listTotals(rows.length, result.total, child.emptyNoun, true), complete: true, help: [...truncationHints, child.showHint] };
461
+ }
462
+ async function showChildObject(session, flags, profile, operation, help, profileName, command, key, known, knownList, defaults, parentParam, parentFlag, parentLabel, childParam) {
463
+ checkReadOperation(operation, help);
464
+ const parent = String(flags[parentFlag]);
465
+ if (!parent.trim())
466
+ throw new AxiError(`--${parentFlag} needs ${parentLabel}`, "VALIDATION_ERROR", [help]);
467
+ const { select, fields } = selectedFields(flags, known, knownList, defaults, help);
468
+ const scopes = scopesFor(flags, profile, help);
469
+ const full = flags.full === true;
470
+ const raw = await session.execute({
471
+ profile,
472
+ operation,
473
+ params: { [parentParam]: parent, [childParam]: String(flags.id) },
474
+ query: { $select: select.join(",") },
475
+ scopes,
476
+ });
477
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
478
+ throw new AxiError("Graph returned a malformed child object body", "GRAPH_ERROR", [
479
+ "Single-object reads carry one object; treat anything else as unknown, not empty",
480
+ ]);
481
+ }
482
+ const { row, truncated } = project(raw, fields, full);
483
+ if (truncated)
484
+ return { [key]: row, help: [fullHint(command, flags, profileName)] };
485
+ return { [key]: row };
486
+ }
487
+ export async function listApplicationFederatedCredentials(session, flags, profile, operation, help, profileName) {
488
+ return listChildCollection(session, flags, profile, operation, help, profileName, {
489
+ command: "entra application federated-credential list",
490
+ key: "federatedCredentials",
491
+ emptyNoun: "federated credentials",
492
+ showHint: `mg-axi entra application federated-credential show --application <application-object-id> --id <credential-id> ${profileHint(profileName)}`,
493
+ parentParam: "application-id",
494
+ parentFlag: "application",
495
+ parentLabel: "the owning application object ID",
496
+ }, KNOWN_FEDERATED, KNOWN_FEDERATED_FIELDS, DEFAULT_FEDERATED_LIST_SELECT);
497
+ }
498
+ export async function showApplicationFederatedCredential(session, flags, profile, operation, help, profileName) {
499
+ return showChildObject(session, flags, profile, operation, help, profileName, "entra application federated-credential show", "federatedCredential", KNOWN_FEDERATED, KNOWN_FEDERATED_FIELDS, DEFAULT_FEDERATED_SHOW_SELECT, "application-id", "application", "the owning application object ID", "federatedIdentityCredential-id");
500
+ }
501
+ export async function listServicePrincipalFederatedCredentials(session, flags, profile, operation, help, profileName) {
502
+ return listChildCollection(session, flags, profile, operation, help, profileName, {
503
+ command: "entra service-principal federated-credential list",
504
+ key: "federatedCredentials",
505
+ emptyNoun: "federated credentials",
506
+ showHint: `mg-axi entra service-principal federated-credential show --service-principal <service-principal-object-id> --id <credential-id> ${profileHint(profileName)}`,
507
+ parentParam: "servicePrincipal-id",
508
+ parentFlag: "service-principal",
509
+ parentLabel: "the owning service-principal object ID",
510
+ }, KNOWN_FEDERATED, KNOWN_FEDERATED_FIELDS, DEFAULT_FEDERATED_LIST_SELECT);
511
+ }
512
+ export async function showServicePrincipalFederatedCredential(session, flags, profile, operation, help, profileName) {
513
+ return showChildObject(session, flags, profile, operation, help, profileName, "entra service-principal federated-credential show", "federatedCredential", KNOWN_FEDERATED, KNOWN_FEDERATED_FIELDS, DEFAULT_FEDERATED_SHOW_SELECT, "servicePrincipal-id", "service-principal", "the owning service-principal object ID", "federatedIdentityCredential-id");
514
+ }
515
+ export async function listServicePrincipalAssignedTo(session, flags, profile, operation, help, profileName) {
516
+ return listChildCollection(session, flags, profile, operation, help, profileName, {
517
+ command: "entra service-principal app-role-assigned-to list",
518
+ key: "appRoleAssignedTo",
519
+ emptyNoun: "app role assignments",
520
+ showHint: `mg-axi entra service-principal app-role-assigned-to show --service-principal <service-principal-object-id> --id <assignment-id> ${profileHint(profileName)}`,
521
+ parentParam: "servicePrincipal-id",
522
+ parentFlag: "service-principal",
523
+ parentLabel: "the resource service-principal object ID",
524
+ }, KNOWN_ASSIGNED, KNOWN_APP_ROLE_FIELDS, DEFAULT_ASSIGNED_LIST_SELECT);
525
+ }
526
+ export async function showServicePrincipalAssignedTo(session, flags, profile, operation, help, profileName) {
527
+ return showChildObject(session, flags, profile, operation, help, profileName, "entra service-principal app-role-assigned-to show", "appRoleAssignedTo", KNOWN_ASSIGNED, KNOWN_APP_ROLE_FIELDS, DEFAULT_ASSIGNED_SHOW_SELECT, "servicePrincipal-id", "service-principal", "the resource service-principal object ID", "appRoleAssignment-id");
528
+ }
529
+ export async function showServicePrincipalAppRoleAssignment(session, flags, profile, operation, help, profileName) {
530
+ return showChildObject(session, flags, profile, operation, help, profileName, "entra service-principal app-role-assignment show", "appRoleAssignment", KNOWN_ASSIGNED, KNOWN_APP_ROLE_FIELDS, DEFAULT_ASSIGNED_SHOW_SELECT, "servicePrincipal-id", "service-principal", "the client service-principal object ID", "appRoleAssignment-id");
531
+ }
@@ -1,17 +1,24 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
- // READ-05: the Entra sign-in and directory-audit read mapping behind
3
- // `mg-axi entra sign-in list/show` and `mg-axi entra directory-audit
4
- // list/show`. Operation construction stays beside its command; the shared
2
+ import { listTotals } from "./list-totals.js";
3
+ // READ-05: the Entra sign-in, directory-audit and provisioning-log read
4
+ // mapping behind `mg-axi entra sign-in list/show`, `mg-axi entra
5
+ // directory-audit list/show` and `mg-axi entra provisioning list/show`.
6
+ // Operation construction stays beside its command; the shared
5
7
  // session owns URLs, credentials, paging, retries and error translation, and
6
8
  // the SDK owns TOON rendering. This module only maps flags to session calls,
7
9
  // bounds every collection query in time, and projects rows for compact output.
8
10
  //
9
11
  // Reviewed against the v1.0 signin-list and directoryaudit-list operation
10
- // documentation on 2026-10-04. Both collections read D/A AuditLog.Read.All;
11
- // delegated sign-in reads additionally accept Global Reader, Reports Reader,
12
- // Security Administrator, Security Operator or Security Reader, while
13
- // delegated directory-audit reads accept Reports Reader, Security
14
- // Administrator or Security Reader. Graph omits appliedConditionalAccessPolicies
12
+ // documentation on 2026-10-04 and the provisioningobjectsummary-list
13
+ // documentation on 2026-10-06. Sign-in and directory-audit collections read
14
+ // D/A AuditLog.Read.All; delegated sign-in reads additionally accept Global
15
+ // Reader, Reports Reader, Security Administrator, Security Operator or
16
+ // Security Reader, while delegated directory-audit reads accept Reports
17
+ // Reader, Security Administrator or Security Reader. The provisioning
18
+ // collection reads D/A AuditLog.Read.All and Directory.Read.All together;
19
+ // delegated reads accept the same five roles as sign-ins, Reports Reader is
20
+ // the least privileged role for the logs, and the tenant needs a P1 or P2
21
+ // licence. Graph omits appliedConditionalAccessPolicies
15
22
  // without a CA-data permission/role, so an absent property is reported as
16
23
  // unavailable rather than invented. Sign-in/audit Graph reporting carries a
17
24
  // conservative P1/P2 deployment prerequisite; retention and premium fields
@@ -54,13 +61,45 @@ export const KNOWN_AUDIT_FIELDS = [
54
61
  ];
55
62
  const SIGNIN_KNOWN = new Set(KNOWN_SIGNIN_FIELDS);
56
63
  const AUDIT_KNOWN = new Set(KNOWN_AUDIT_FIELDS);
64
+ // The full reviewed provisioningObjectSummary property set. None of these
65
+ // is credential material (no password, key or secret properties), and the
66
+ // known-set gate rejects anything else before credentials; the session's
67
+ // shared redaction stays the backstop for secret-shaped values.
68
+ export const KNOWN_PROVISIONING_FIELDS = [
69
+ "id",
70
+ "activityDateTime",
71
+ "tenantId",
72
+ "jobId",
73
+ "cycleId",
74
+ "changeId",
75
+ "action",
76
+ "durationInMilliseconds",
77
+ "sourceSystem",
78
+ "sourceIdentity",
79
+ "targetSystem",
80
+ "targetIdentity",
81
+ "provisioningStatusInfo",
82
+ "provisioningSteps",
83
+ "modifiedProperties",
84
+ "servicePrincipal",
85
+ "initiatedBy",
86
+ ];
87
+ const PROVISIONING_KNOWN = new Set(KNOWN_PROVISIONING_FIELDS);
57
88
  // Compact list rows: identifier, time, who/what happened, and outcome.
58
89
  const DEFAULT_SIGNIN_LIST_SELECT = ["id", "createdDateTime", "userPrincipalName", "appDisplayName"];
59
90
  const DEFAULT_AUDIT_LIST_SELECT = ["id", "activityDateTime", "activityDisplayName", "result"];
91
+ const DEFAULT_PROVISIONING_LIST_SELECT = ["id", "activityDateTime", "action", "provisioningStatusInfo"];
60
92
  // Show rows: the full reviewed server set for one object.
61
93
  const DEFAULT_SIGNIN_SHOW_SELECT = [...KNOWN_SIGNIN_FIELDS];
62
94
  const DEFAULT_AUDIT_SHOW_SELECT = [...KNOWN_AUDIT_FIELDS];
95
+ const DEFAULT_PROVISIONING_SHOW_SELECT = [...KNOWN_PROVISIONING_FIELDS];
63
96
  export const DEFAULT_DELEGATED_SCOPES = ["https://graph.microsoft.com/AuditLog.Read.All"];
97
+ // Provisioning reads need both directory scopes; the pair is the documented
98
+ // least-privileged delegated set and both names already sit in READ_SCOPES.
99
+ export const DEFAULT_PROVISIONING_SCOPES = [
100
+ "https://graph.microsoft.com/AuditLog.Read.All",
101
+ "https://graph.microsoft.com/Directory.Read.All",
102
+ ];
64
103
  const TRUNCATE_AT = 500;
65
104
  // Graph omits this property without CA-data access instead of failing, so an
66
105
  // absent value reports the missing prerequisite rather than an empty result.
@@ -87,7 +126,7 @@ function fieldList(raw, flag, known, knownList, help) {
87
126
  }
88
127
  return fields;
89
128
  }
90
- function scopesFor(flags, profile, help) {
129
+ function scopesFor(flags, profile, help, defaultScopes = DEFAULT_DELEGATED_SCOPES) {
91
130
  if (profile.mode === "application") {
92
131
  if (flags.scopes !== undefined) {
93
132
  throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
@@ -95,7 +134,7 @@ function scopesFor(flags, profile, help) {
95
134
  return undefined;
96
135
  }
97
136
  if (flags.scopes === undefined)
98
- return [...DEFAULT_DELEGATED_SCOPES];
137
+ return [...defaultScopes];
99
138
  const scopes = String(flags.scopes)
100
139
  .split(",")
101
140
  .map(scope => scope.trim())
@@ -226,10 +265,16 @@ const AUDIT_DENIAL_HINTS = [
226
265
  "Directory-audit reads need AuditLog.Read.All plus a supported directory role: Reports Reader, Security Administrator or Security Reader for delegated access, or admin-consented AuditLog.Read.All for application access",
227
266
  "Audit Graph reporting carries a conservative P1/P2 deployment prerequisite; retention stays a separate constraint",
228
267
  ];
268
+ const PROVISIONING_DENIAL_HINTS = [
269
+ "Provisioning-log reads need AuditLog.Read.All and Directory.Read.All plus a supported directory role: Global Reader, Reports Reader, Security Administrator, Security Operator or Security Reader for delegated access, or admin-consented AuditLog.Read.All and Directory.Read.All for application access",
270
+ "Provisioning logs need a Microsoft Entra ID P1 or P2 licence on the tenant; retention is 30 days on premium and 7 days on free",
271
+ ];
229
272
  const SIGNIN_LIST = {
230
273
  noun: "sign-in",
274
+ plural: "sign-ins",
231
275
  key: "signIns",
232
276
  dateField: "createdDateTime",
277
+ defaultScopes: DEFAULT_DELEGATED_SCOPES,
233
278
  known: SIGNIN_KNOWN,
234
279
  knownList: KNOWN_SIGNIN_FIELDS,
235
280
  defaultSelect: DEFAULT_SIGNIN_LIST_SELECT,
@@ -243,8 +288,10 @@ const SIGNIN_LIST = {
243
288
  };
244
289
  const AUDIT_LIST = {
245
290
  noun: "directory-audit",
291
+ plural: "directory audits",
246
292
  key: "directoryAudits",
247
293
  dateField: "activityDateTime",
294
+ defaultScopes: DEFAULT_DELEGATED_SCOPES,
248
295
  known: AUDIT_KNOWN,
249
296
  knownList: KNOWN_AUDIT_FIELDS,
250
297
  defaultSelect: DEFAULT_AUDIT_LIST_SELECT,
@@ -256,6 +303,23 @@ const AUDIT_LIST = {
256
303
  "0 directory audits matched in this window; widen --since/--until or loosen --filter - the absence of results is the answer, not an error",
257
304
  ],
258
305
  };
306
+ const PROVISIONING_LIST = {
307
+ noun: "provisioning",
308
+ plural: "provisioning events",
309
+ key: "provisioning",
310
+ dateField: "activityDateTime",
311
+ defaultScopes: DEFAULT_PROVISIONING_SCOPES,
312
+ known: PROVISIONING_KNOWN,
313
+ knownList: KNOWN_PROVISIONING_FIELDS,
314
+ defaultSelect: DEFAULT_PROVISIONING_LIST_SELECT,
315
+ unavailable: {},
316
+ denialHints: PROVISIONING_DENIAL_HINTS,
317
+ showHint: "mg-axi entra provisioning show --id <provisioning-id>",
318
+ emptyHints: profileName => [
319
+ `mg-axi entra provisioning list --since <earlier-iso-time> ${profileHint(profileName)}`,
320
+ "0 provisioning events matched in this window; widen --since/--until or loosen --filter - the absence of results is the answer, not an error",
321
+ ],
322
+ };
259
323
  async function listLogs(shape, session, flags, profile, operation, help, profileName) {
260
324
  const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
261
325
  if (cursor !== undefined && !cursor.trim())
@@ -277,7 +341,7 @@ async function listLogs(shape, session, flags, profile, operation, help, profile
277
341
  }
278
342
  const savedSelect = saved?.$select;
279
343
  const { select, fields } = selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help);
280
- const scopes = scopesFor(flags, profile, help);
344
+ const scopes = scopesFor(flags, profile, help, shape.defaultScopes);
281
345
  const full = flags.full === true;
282
346
  const filter = boundedFilter(shape.dateField, flags, help);
283
347
  const query = { $select: select.join(",") };
@@ -313,21 +377,23 @@ async function listLogs(shape, session, flags, profile, operation, help, profile
313
377
  if (!result.complete) {
314
378
  return {
315
379
  [shape.key]: rows,
316
- count: { returned: rows.length, complete: false, reason: result.reason },
380
+ ...listTotals(rows.length, result.total, shape.plural, false),
381
+ complete: false,
382
+ reason: result.reason,
317
383
  cursor: result.cursor,
318
384
  help: [...truncationHints, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`, showHint],
319
385
  };
320
386
  }
321
- const count = { returned: rows.length, complete: true };
322
387
  if (!rows.length) {
323
- return { [shape.key]: rows, count, help: shape.emptyHints(profileName) };
388
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.plural, true), complete: true, help: shape.emptyHints(profileName) };
324
389
  }
325
- return { [shape.key]: rows, count, help: [...truncationHints, showHint] };
390
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.plural, true), complete: true, help: [...truncationHints, showHint] };
326
391
  }
327
392
  const SIGNIN_SHOW = {
328
393
  noun: "sign-in",
329
394
  key: "signIn",
330
395
  param: "signIn-id",
396
+ defaultScopes: DEFAULT_DELEGATED_SCOPES,
331
397
  known: SIGNIN_KNOWN,
332
398
  knownList: KNOWN_SIGNIN_FIELDS,
333
399
  defaultSelect: DEFAULT_SIGNIN_SHOW_SELECT,
@@ -339,6 +405,7 @@ const AUDIT_SHOW = {
339
405
  noun: "directory-audit",
340
406
  key: "directoryAudit",
341
407
  param: "directoryAudit-id",
408
+ defaultScopes: DEFAULT_DELEGATED_SCOPES,
342
409
  known: AUDIT_KNOWN,
343
410
  knownList: KNOWN_AUDIT_FIELDS,
344
411
  defaultSelect: DEFAULT_AUDIT_SHOW_SELECT,
@@ -346,9 +413,21 @@ const AUDIT_SHOW = {
346
413
  denialHints: AUDIT_DENIAL_HINTS,
347
414
  idFlag: "<directory-audit-id>",
348
415
  };
416
+ const PROVISIONING_SHOW = {
417
+ noun: "provisioning",
418
+ key: "provisioning",
419
+ param: "provisioningObjectSummary-id",
420
+ defaultScopes: DEFAULT_PROVISIONING_SCOPES,
421
+ known: PROVISIONING_KNOWN,
422
+ knownList: KNOWN_PROVISIONING_FIELDS,
423
+ defaultSelect: DEFAULT_PROVISIONING_SHOW_SELECT,
424
+ unavailable: {},
425
+ denialHints: PROVISIONING_DENIAL_HINTS,
426
+ idFlag: "<provisioning-id>",
427
+ };
349
428
  async function showLog(shape, session, flags, profile, operation, help, profileName) {
350
429
  const { select, fields } = selectedFields(flags, shape.defaultSelect, shape.known, shape.knownList, help);
351
- const scopes = scopesFor(flags, profile, help);
430
+ const scopes = scopesFor(flags, profile, help, shape.defaultScopes);
352
431
  const full = flags.full === true;
353
432
  const raw = await withGuidance(shape.denialHints, () => session.execute({
354
433
  profile,
@@ -379,3 +458,9 @@ export async function listDirectoryAudits(session, flags, profile, operation, he
379
458
  export async function showDirectoryAudit(session, flags, profile, operation, help, profileName) {
380
459
  return showLog(AUDIT_SHOW, session, flags, profile, operation, help, profileName);
381
460
  }
461
+ export async function listProvisioning(session, flags, profile, operation, help, profileName) {
462
+ return listLogs(PROVISIONING_LIST, session, flags, profile, operation, help, profileName);
463
+ }
464
+ export async function showProvisioning(session, flags, profile, operation, help, profileName) {
465
+ return showLog(PROVISIONING_SHOW, session, flags, profile, operation, help, profileName);
466
+ }
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // READ-04: the Entra authentication-method and registration-report read
3
4
  // mapping behind `mg-axi entra user authentication-method list` and
4
5
  // `mg-axi entra registration list`. Operation construction stays beside its
@@ -203,6 +204,7 @@ const REPORT_DENIAL_HINTS = [
203
204
  const METHOD_LIST = {
204
205
  command: "entra user authentication-method list",
205
206
  key: "authenticationMethods",
207
+ noun: "authentication methods",
206
208
  known: KNOWN_METHODS,
207
209
  knownList: KNOWN_METHOD_FIELDS,
208
210
  defaultSelect: DEFAULT_METHOD_SELECT,
@@ -218,6 +220,7 @@ const METHOD_LIST = {
218
220
  const REPORT_LIST = {
219
221
  command: "entra registration list",
220
222
  key: "registrationDetails",
223
+ noun: "registration rows",
221
224
  known: KNOWN_REPORT,
222
225
  knownList: KNOWN_REPORT_FIELDS,
223
226
  defaultSelect: DEFAULT_REPORT_SELECT,
@@ -274,16 +277,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
274
277
  if (!result.complete) {
275
278
  return {
276
279
  [shape.key]: rows,
277
- count: { returned: rows.length, complete: false, reason: result.reason },
280
+ ...listTotals(rows.length, result.total, shape.noun, false),
281
+ complete: false,
282
+ reason: result.reason,
278
283
  cursor: result.cursor,
279
284
  help: [...truncationHints, `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, ...standing],
280
285
  };
281
286
  }
282
- const count = { returned: rows.length, complete: true };
283
287
  if (!rows.length) {
284
- return { [shape.key]: rows, count, help: [shape.emptyNote, ...standing] };
288
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
285
289
  }
286
- return { [shape.key]: rows, count, help: [...truncationHints, ...standing] };
290
+ return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
287
291
  }
288
292
  export async function listAuthenticationMethods(session, flags, profile, operation, help, profileName) {
289
293
  const user = String(flags.user);
@@ -1,4 +1,5 @@
1
1
  import { AxiError } from "axi-sdk-js";
2
+ import { listTotals } from "./list-totals.js";
2
3
  // EXT-01 certificate-auth subfamily: the read mapping behind
3
4
  // `mg-axi entra certificate-auth-pki list/show/count` and
4
5
  // `mg-axi entra certificate-authority list/show/count`. Operation
@@ -242,16 +243,17 @@ export async function listCertificateAuthPkis(session, flags, profile, operation
242
243
  if (!result.complete) {
243
244
  return {
244
245
  certificateAuthPkis: pkis,
245
- count: { returned: pkis.length, complete: false, reason: result.reason },
246
+ ...listTotals(pkis.length, result.total, "PKI configurations", false),
247
+ complete: false,
248
+ reason: result.reason,
246
249
  cursor: result.cursor,
247
250
  help: [...truncationHints, resumeHint(profileName), showHint, EMPTY_PKI_NOTE],
248
251
  };
249
252
  }
250
- const count = { returned: pkis.length, complete: true };
251
253
  if (!pkis.length) {
252
- return { certificateAuthPkis: pkis, count, help: [EMPTY_PKI_NOTE] };
254
+ return { certificateAuthPkis: pkis, ...listTotals(pkis.length, result.total, "PKI configurations", true), complete: true, help: [EMPTY_PKI_NOTE] };
253
255
  }
254
- return { certificateAuthPkis: pkis, count, help: [...truncationHints, showHint, EMPTY_PKI_NOTE] };
256
+ return { certificateAuthPkis: pkis, ...listTotals(pkis.length, result.total, "PKI configurations", true), complete: true, help: [...truncationHints, showHint, EMPTY_PKI_NOTE] };
255
257
  }
256
258
  export async function showCertificateAuthPki(session, flags, profile, operation, help, profileName) {
257
259
  const { select, fields } = selectedFields(flags, KNOWN_PKIS, KNOWN_PKI_FIELDS, DEFAULT_PKI_SHOW_SELECT, help, "PKI");
@@ -324,20 +326,22 @@ export async function listCertificateAuthorities(session, flags, profile, operat
324
326
  if (!result.complete) {
325
327
  return {
326
328
  certificateAuthorities: authorities,
327
- count: { returned: authorities.length, complete: false, reason: result.reason },
329
+ ...listTotals(authorities.length, result.total, "certificate authorities", false),
330
+ complete: false,
331
+ reason: result.reason,
328
332
  cursor: result.cursor,
329
333
  help: [...truncationHints, resumeHint(profileName), showHint, CERT_BLOB_NOTE],
330
334
  };
331
335
  }
332
- const count = { returned: authorities.length, complete: true };
333
336
  if (!authorities.length) {
334
337
  return {
335
338
  certificateAuthorities: authorities,
336
- count,
339
+ ...listTotals(authorities.length, result.total, "certificate authorities", true),
340
+ complete: true,
337
341
  help: ["0 certificate authorities matched; the absence of results is the answer, not an error", CERT_BLOB_NOTE],
338
342
  };
339
343
  }
340
- return { certificateAuthorities: authorities, count, help: [...truncationHints, showHint, CERT_BLOB_NOTE] };
344
+ return { certificateAuthorities: authorities, ...listTotals(authorities.length, result.total, "certificate authorities", true), complete: true, help: [...truncationHints, showHint, CERT_BLOB_NOTE] };
341
345
  }
342
346
  export async function showCertificateAuthority(session, flags, profile, operation, help, profileName) {
343
347
  const { select, fields } = selectedFields(flags, KNOWN_CAS, KNOWN_CA_FIELDS, DEFAULT_CA_SHOW_SELECT, help, "certificate-authority");