@knowttl/mg-axi 0.1.1 → 0.1.2
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 +149 -38
- package/dist/api.js +134 -8
- package/dist/bin/mg-axi-hook.js +31 -0
- package/dist/catalogue.js +390 -2
- package/dist/cli.js +116 -8
- package/dist/docs.js +45 -5
- package/dist/doctor.js +2 -2
- package/dist/entra-access-reviews.js +11 -4
- package/dist/entra-apps.js +13 -8
- package/dist/entra-audit-logs.js +8 -4
- package/dist/entra-auth-methods.js +8 -4
- package/dist/entra-certificate-auth.js +12 -8
- package/dist/entra-conditional-access.js +9 -4
- package/dist/entra-contacts.js +19 -12
- package/dist/entra-contracts.js +7 -4
- package/dist/entra-custom-security-attributes.js +19 -12
- package/dist/entra-data-policy-operations.js +7 -4
- package/dist/entra-delegated-admin.js +55 -6
- package/dist/entra-deleted-items.js +6 -4
- package/dist/entra-directory-objects.js +6 -4
- package/dist/entra-directory.js +19 -12
- package/dist/entra-domains.js +13 -8
- package/dist/entra-entitlement-management.js +12 -4
- package/dist/entra-group-pim.js +431 -0
- package/dist/entra-groups.js +13 -8
- package/dist/entra-lifecycle-workflows.js +136 -9
- package/dist/entra-organization.js +13 -8
- package/dist/entra-risk-prevention.js +436 -0
- package/dist/entra-risk.js +213 -5
- package/dist/entra-roles.js +10 -4
- package/dist/entra-subscriptions.js +6 -4
- package/dist/entra-users.js +7 -4
- package/dist/graph-session.js +29 -5
- package/dist/hook.js +84 -0
- package/dist/list-totals.js +52 -0
- package/dist/setup.js +5 -0
- package/docs/coverage.md +79 -13
- package/inventory/operations.json +2 -2
- package/package.json +4 -3
- package/skills/mg-axi/SKILL.md +56 -1
package/dist/entra-domains.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-01 domains subfamily: the read mapping behind `mg-axi entra domain
|
|
3
4
|
// list/show`, the per-domain verification and service-configuration DNS
|
|
4
5
|
// record reads, and the top-level domain DNS record reads. Operation
|
|
@@ -254,20 +255,22 @@ export async function listDomains(session, flags, profile, operation, help, prof
|
|
|
254
255
|
if (!result.complete) {
|
|
255
256
|
return {
|
|
256
257
|
domains,
|
|
257
|
-
|
|
258
|
+
...listTotals(domains.length, result.total, "domains", false),
|
|
259
|
+
complete: false,
|
|
260
|
+
reason: result.reason,
|
|
258
261
|
cursor: result.cursor,
|
|
259
262
|
help: [...truncationHints, resumeHint(profileName), showHint, filterNote],
|
|
260
263
|
};
|
|
261
264
|
}
|
|
262
|
-
const count = { returned: domains.length, complete: true };
|
|
263
265
|
if (!domains.length) {
|
|
264
266
|
return {
|
|
265
267
|
domains,
|
|
266
|
-
|
|
268
|
+
...listTotals(domains.length, result.total, "domains", true),
|
|
269
|
+
complete: true,
|
|
267
270
|
help: [filterNote, "0 domains matched; the absence of results is the answer, not an error"],
|
|
268
271
|
};
|
|
269
272
|
}
|
|
270
|
-
return { domains,
|
|
273
|
+
return { domains, ...listTotals(domains.length, result.total, "domains", true), complete: true, help: [...truncationHints, showHint, filterNote] };
|
|
271
274
|
}
|
|
272
275
|
export async function showDomain(session, flags, profile, operation, help, profileName) {
|
|
273
276
|
const { select, fields } = selectedFields(flags, KNOWN_DOMAINS, KNOWN_DOMAIN_FIELDS, DEFAULT_DOMAIN_SHOW_SELECT, help);
|
|
@@ -321,20 +324,22 @@ async function listDnsRecords(session, flags, profile, operation, help, profileN
|
|
|
321
324
|
if (!result.complete) {
|
|
322
325
|
return {
|
|
323
326
|
[command.collectionKey]: records,
|
|
324
|
-
|
|
327
|
+
...listTotals(records.length, result.total, "DNS records", false),
|
|
328
|
+
complete: false,
|
|
329
|
+
reason: result.reason,
|
|
325
330
|
cursor: result.cursor,
|
|
326
331
|
help: [...truncationHints, resumeHint(profileName), showHint, kindHint],
|
|
327
332
|
};
|
|
328
333
|
}
|
|
329
|
-
const count = { returned: records.length, complete: true };
|
|
330
334
|
if (!records.length) {
|
|
331
335
|
return {
|
|
332
336
|
[command.collectionKey]: records,
|
|
333
|
-
|
|
337
|
+
...listTotals(records.length, result.total, "DNS records", true),
|
|
338
|
+
complete: true,
|
|
334
339
|
help: [kindHint, "0 DNS records matched; the absence of results is the answer, not an error"],
|
|
335
340
|
};
|
|
336
341
|
}
|
|
337
|
-
return { [command.collectionKey]: records,
|
|
342
|
+
return { [command.collectionKey]: records, ...listTotals(records.length, result.total, "DNS records", true), complete: true, help: [...truncationHints, showHint, kindHint] };
|
|
338
343
|
}
|
|
339
344
|
async function showDnsRecord(session, flags, profile, operation, help, profileName, command) {
|
|
340
345
|
const { select, fields } = selectedFields(flags, KNOWN_DNS, KNOWN_DNS_FIELDS, DEFAULT_DNS_SHOW_SELECT, help);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-02 entitlement-management reads: the read-only catalog and
|
|
3
4
|
// access-package mapping plus the assignment and request reads. See
|
|
4
5
|
// README.md for the supported commands and usage. Operation construction
|
|
@@ -348,6 +349,7 @@ function flattenRequestLinks(source) {
|
|
|
348
349
|
const CATALOG_LIST = {
|
|
349
350
|
command: "entra entitlement catalog list",
|
|
350
351
|
key: "catalogs",
|
|
352
|
+
noun: "catalogs",
|
|
351
353
|
known: CATALOG_KNOWN,
|
|
352
354
|
knownList: KNOWN_CATALOG_FIELDS,
|
|
353
355
|
defaultSelect: DEFAULT_CATALOG_LIST_SELECT,
|
|
@@ -360,6 +362,7 @@ const CATALOG_LIST = {
|
|
|
360
362
|
const ACCESS_PACKAGE_LIST = {
|
|
361
363
|
command: "entra entitlement access-package list",
|
|
362
364
|
key: "accessPackages",
|
|
365
|
+
noun: "access packages",
|
|
363
366
|
known: ACCESS_PACKAGE_KNOWN,
|
|
364
367
|
knownList: KNOWN_ACCESS_PACKAGE_FIELDS,
|
|
365
368
|
defaultSelect: DEFAULT_ACCESS_PACKAGE_LIST_SELECT,
|
|
@@ -372,6 +375,7 @@ const ACCESS_PACKAGE_LIST = {
|
|
|
372
375
|
const ASSIGNMENT_POLICY_LIST = {
|
|
373
376
|
command: "entra entitlement assignment-policy list",
|
|
374
377
|
key: "assignmentPolicies",
|
|
378
|
+
noun: "assignment policies",
|
|
375
379
|
known: ASSIGNMENT_POLICY_KNOWN,
|
|
376
380
|
knownList: KNOWN_ASSIGNMENT_POLICY_FIELDS,
|
|
377
381
|
defaultSelect: DEFAULT_ASSIGNMENT_POLICY_LIST_SELECT,
|
|
@@ -384,6 +388,7 @@ const ASSIGNMENT_POLICY_LIST = {
|
|
|
384
388
|
const RESOURCE_ROLE_SCOPE_LIST = {
|
|
385
389
|
command: "entra entitlement resource-role-scope list",
|
|
386
390
|
key: "resourceRoleScopes",
|
|
391
|
+
noun: "resource-role scopes",
|
|
387
392
|
known: RESOURCE_ROLE_SCOPE_KNOWN,
|
|
388
393
|
knownList: KNOWN_RESOURCE_ROLE_SCOPE_FIELDS,
|
|
389
394
|
defaultSelect: DEFAULT_RESOURCE_ROLE_SCOPE_LIST_SELECT,
|
|
@@ -396,6 +401,7 @@ const RESOURCE_ROLE_SCOPE_LIST = {
|
|
|
396
401
|
const ASSIGNMENT_LIST = {
|
|
397
402
|
command: "entra entitlement assignment list",
|
|
398
403
|
key: "assignments",
|
|
404
|
+
noun: "assignments",
|
|
399
405
|
known: ASSIGNMENT_KNOWN,
|
|
400
406
|
knownList: KNOWN_ASSIGNMENT_FIELDS,
|
|
401
407
|
defaultSelect: DEFAULT_ASSIGNMENT_LIST_SELECT,
|
|
@@ -413,6 +419,7 @@ const ASSIGNMENT_LIST = {
|
|
|
413
419
|
const ASSIGNMENT_REQUEST_LIST = {
|
|
414
420
|
command: "entra entitlement assignment-request list",
|
|
415
421
|
key: "assignmentRequests",
|
|
422
|
+
noun: "assignment requests",
|
|
416
423
|
known: ASSIGNMENT_REQUEST_KNOWN,
|
|
417
424
|
knownList: KNOWN_ASSIGNMENT_REQUEST_FIELDS,
|
|
418
425
|
defaultSelect: DEFAULT_ASSIGNMENT_REQUEST_LIST_SELECT,
|
|
@@ -501,16 +508,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
|
|
|
501
508
|
if (!result.complete) {
|
|
502
509
|
return {
|
|
503
510
|
[shape.key]: rows,
|
|
504
|
-
|
|
511
|
+
...listTotals(rows.length, result.total, shape.noun, false),
|
|
512
|
+
complete: false,
|
|
513
|
+
reason: result.reason,
|
|
505
514
|
cursor: result.cursor,
|
|
506
515
|
help: [...truncationHints, resumeHint, ...standing],
|
|
507
516
|
};
|
|
508
517
|
}
|
|
509
|
-
const count = { returned: rows.length, complete: true };
|
|
510
518
|
if (!rows.length) {
|
|
511
|
-
return { [shape.key]: rows,
|
|
519
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
|
|
512
520
|
}
|
|
513
|
-
return { [shape.key]: rows,
|
|
521
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
|
|
514
522
|
}
|
|
515
523
|
async function showOne(key, command, known, knownList, defaultSelect, session, flags, profile, operation, help, profileName, params, malformed, extra) {
|
|
516
524
|
const { select, fields, fetch } = selectedFields(flags, defaultSelect, known, knownList, help, extra?.synthetic);
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
3
|
+
// EXT-02 group PIM schedule reads: the read-only schedule and instance
|
|
4
|
+
// mapping behind `mg-axi entra pim group-assignment-schedule list/show`,
|
|
5
|
+
// `mg-axi entra pim group-assignment-instance list/show`,
|
|
6
|
+
// `mg-axi entra pim group-eligibility-schedule list/show`,
|
|
7
|
+
// `mg-axi entra pim group-eligibility-instance list/show` and
|
|
8
|
+
// `mg-axi entra pim group-eligibility-request list/show`.
|
|
9
|
+
// Operation construction stays beside its command; the shared session owns
|
|
10
|
+
// URLs, credentials, paging, retries and error translation, and the SDK owns
|
|
11
|
+
// TOON rendering. This module only maps flags to session calls and projects
|
|
12
|
+
// rows for compact output.
|
|
13
|
+
//
|
|
14
|
+
// Reviewed against the v1.0 privilegedaccessgroup-list-assignmentschedules,
|
|
15
|
+
// privilegedaccessgroupassignmentschedule-get,
|
|
16
|
+
// privilegedaccessgroup-list-assignmentscheduleinstances,
|
|
17
|
+
// privilegedaccessgroupassignmentscheduleinstance-get,
|
|
18
|
+
// privilegedaccessgroup-list-eligibilityschedules,
|
|
19
|
+
// privilegedaccessgroupeligibilityschedule-get,
|
|
20
|
+
// privilegedaccessgroup-list-eligibilityscheduleinstances,
|
|
21
|
+
// privilegedaccessgroupeligibilityscheduleinstance-get,
|
|
22
|
+
// privilegedaccessgroup-list-eligibilityschedulerequests and
|
|
23
|
+
// privilegedaccessgroupeligibilityschedulerequest-get operation
|
|
24
|
+
// documentation and the privilegedAccessGroupAssignmentSchedule,
|
|
25
|
+
// privilegedAccessGroupAssignmentScheduleInstance,
|
|
26
|
+
// privilegedAccessGroupEligibilitySchedule,
|
|
27
|
+
// privilegedAccessGroupEligibilityScheduleInstance and
|
|
28
|
+
// privilegedAccessGroupEligibilityScheduleRequest resource contracts on
|
|
29
|
+
// 2026-10-06, with a second check against the Graph permissions reference
|
|
30
|
+
// and the ID Governance licensing fundamentals. Assignment reads take D/A
|
|
31
|
+
// PrivilegedAssignmentSchedule.Read.AzureADGroup; eligibility reads take
|
|
32
|
+
// D/A PrivilegedEligibilitySchedule.Read.AzureADGroup. Delegated callers
|
|
33
|
+
// additionally need owner/member of the group or a supported directory
|
|
34
|
+
// role (Global Reader or Privileged Role Administrator for role-assignable
|
|
35
|
+
// groups; Global Reader, Directory Writer, Groups Administrator, Identity
|
|
36
|
+
// Governance Administrator or User Administrator otherwise), scoped at
|
|
37
|
+
// directory level; delegated personal Microsoft accounts are not
|
|
38
|
+
// supported. PIM for Groups needs P2 or ID Governance.
|
|
39
|
+
//
|
|
40
|
+
// Every list requires $filter (eq) scoped to a groupId or a principalId,
|
|
41
|
+
// so --filter is required and must name one of them; the requirement is
|
|
42
|
+
// validated before credentials. Lists document $select/$filter/$expand, so
|
|
43
|
+
// --filter passes through as plain $filter with no $count or
|
|
44
|
+
// ConsistencyLevel attached; singles document $select with $expand.
|
|
45
|
+
// These collections document no $count contract, so totals stay honest as
|
|
46
|
+
// count { returned, complete } like the extended role PIM reads rather
|
|
47
|
+
// than the shared list-totals helper, which applies where $count=true is
|
|
48
|
+
// sent. Deferred with reason, never built on a write scope:
|
|
49
|
+
// assignmentScheduleRequests list/get take
|
|
50
|
+
// PrivilegedAssignmentSchedule.ReadWrite.AzureADGroup as least privilege.
|
|
51
|
+
// Out of scope: approvals, filterByCurrentUser functions, nested
|
|
52
|
+
// group/principal/activatedUsing/targetSchedule navigation (scalar linkage
|
|
53
|
+
// ids such as assignmentScheduleId stay in), resources, every
|
|
54
|
+
// count/ref/cast tail, all mutations and beta.
|
|
55
|
+
// Schedules carry the full lifecycle (scheduleInfo, status, audit stamps);
|
|
56
|
+
// instances carry one provisioned window plus its parent schedule linkage;
|
|
57
|
+
// requests carry the ask (action, justification, scheduleInfo, ticketInfo)
|
|
58
|
+
// plus its outcome (status, approvalId, targetScheduleId).
|
|
59
|
+
export const KNOWN_ASSIGNMENT_SCHEDULE_FIELDS = [
|
|
60
|
+
"id",
|
|
61
|
+
"accessId",
|
|
62
|
+
"assignmentType",
|
|
63
|
+
"memberType",
|
|
64
|
+
"principalId",
|
|
65
|
+
"groupId",
|
|
66
|
+
"status",
|
|
67
|
+
"scheduleInfo",
|
|
68
|
+
"createdDateTime",
|
|
69
|
+
"modifiedDateTime",
|
|
70
|
+
"createdUsing",
|
|
71
|
+
];
|
|
72
|
+
export const KNOWN_ASSIGNMENT_INSTANCE_FIELDS = [
|
|
73
|
+
"id",
|
|
74
|
+
"accessId",
|
|
75
|
+
"assignmentType",
|
|
76
|
+
"memberType",
|
|
77
|
+
"principalId",
|
|
78
|
+
"groupId",
|
|
79
|
+
"assignmentScheduleId",
|
|
80
|
+
"startDateTime",
|
|
81
|
+
"endDateTime",
|
|
82
|
+
];
|
|
83
|
+
export const KNOWN_ELIGIBILITY_SCHEDULE_FIELDS = [
|
|
84
|
+
"id",
|
|
85
|
+
"accessId",
|
|
86
|
+
"memberType",
|
|
87
|
+
"principalId",
|
|
88
|
+
"groupId",
|
|
89
|
+
"status",
|
|
90
|
+
"scheduleInfo",
|
|
91
|
+
"createdDateTime",
|
|
92
|
+
"modifiedDateTime",
|
|
93
|
+
"createdUsing",
|
|
94
|
+
];
|
|
95
|
+
export const KNOWN_ELIGIBILITY_INSTANCE_FIELDS = [
|
|
96
|
+
"id",
|
|
97
|
+
"accessId",
|
|
98
|
+
"memberType",
|
|
99
|
+
"principalId",
|
|
100
|
+
"groupId",
|
|
101
|
+
"eligibilityScheduleId",
|
|
102
|
+
"startDateTime",
|
|
103
|
+
"endDateTime",
|
|
104
|
+
];
|
|
105
|
+
// Requests narrow to the ask plus its outcome: createdBy is an identitySet
|
|
106
|
+
// for a later slice and customData is documented as unused, so neither is
|
|
107
|
+
// selectable here. Justification is free text and rides only behind an
|
|
108
|
+
// explicit --select, never in a default.
|
|
109
|
+
export const KNOWN_ELIGIBILITY_REQUEST_FIELDS = [
|
|
110
|
+
"id",
|
|
111
|
+
"accessId",
|
|
112
|
+
"action",
|
|
113
|
+
"status",
|
|
114
|
+
"principalId",
|
|
115
|
+
"groupId",
|
|
116
|
+
"justification",
|
|
117
|
+
"scheduleInfo",
|
|
118
|
+
"ticketInfo",
|
|
119
|
+
"createdDateTime",
|
|
120
|
+
"completedDateTime",
|
|
121
|
+
"approvalId",
|
|
122
|
+
"targetScheduleId",
|
|
123
|
+
"isValidationOnly",
|
|
124
|
+
];
|
|
125
|
+
const ASSIGNMENT_SCHEDULE_KNOWN = new Set(KNOWN_ASSIGNMENT_SCHEDULE_FIELDS);
|
|
126
|
+
const ASSIGNMENT_INSTANCE_KNOWN = new Set(KNOWN_ASSIGNMENT_INSTANCE_FIELDS);
|
|
127
|
+
const ELIGIBILITY_SCHEDULE_KNOWN = new Set(KNOWN_ELIGIBILITY_SCHEDULE_FIELDS);
|
|
128
|
+
const ELIGIBILITY_INSTANCE_KNOWN = new Set(KNOWN_ELIGIBILITY_INSTANCE_FIELDS);
|
|
129
|
+
const ELIGIBILITY_REQUEST_KNOWN = new Set(KNOWN_ELIGIBILITY_REQUEST_FIELDS);
|
|
130
|
+
// Compact rows: identifiers plus the correlation keys. groupId and
|
|
131
|
+
// principalId are the required scoping keys; accessId names member versus
|
|
132
|
+
// owner; assignmentType names Assigned versus Activated; memberType names
|
|
133
|
+
// how the row reaches the principal (for example Direct).
|
|
134
|
+
const DEFAULT_ASSIGNMENT_SCHEDULE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "assignmentType"];
|
|
135
|
+
const DEFAULT_ASSIGNMENT_INSTANCE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "assignmentType"];
|
|
136
|
+
const DEFAULT_ELIGIBILITY_SCHEDULE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "memberType"];
|
|
137
|
+
const DEFAULT_ELIGIBILITY_INSTANCE_LIST_SELECT = ["id", "principalId", "groupId", "accessId", "memberType"];
|
|
138
|
+
const DEFAULT_ELIGIBILITY_REQUEST_LIST_SELECT = ["id", "action", "status", "principalId", "groupId", "accessId"];
|
|
139
|
+
const DEFAULT_ELIGIBILITY_REQUEST_SHOW_SELECT = KNOWN_ELIGIBILITY_REQUEST_FIELDS.filter(field => field !== "justification");
|
|
140
|
+
export const DEFAULT_ASSIGNMENT_SCOPES = ["https://graph.microsoft.com/PrivilegedAssignmentSchedule.Read.AzureADGroup"];
|
|
141
|
+
export const DEFAULT_ELIGIBILITY_SCOPES = ["https://graph.microsoft.com/PrivilegedEligibilitySchedule.Read.AzureADGroup"];
|
|
142
|
+
const TRUNCATE_AT = 500;
|
|
143
|
+
function fieldList(raw, flag, known, knownList, help) {
|
|
144
|
+
const parts = String(raw)
|
|
145
|
+
.split(",")
|
|
146
|
+
.map(part => part.trim())
|
|
147
|
+
.filter(part => part.length > 0);
|
|
148
|
+
if (!parts.length)
|
|
149
|
+
throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
|
|
150
|
+
const fields = [];
|
|
151
|
+
for (const part of parts) {
|
|
152
|
+
if (!known.has(part)) {
|
|
153
|
+
throw new AxiError(`Unknown property ${part} in --${flag}`, "VALIDATION_ERROR", [
|
|
154
|
+
help,
|
|
155
|
+
`Known properties: ${knownList.join(", ")}`,
|
|
156
|
+
]);
|
|
157
|
+
}
|
|
158
|
+
if (!fields.includes(part))
|
|
159
|
+
fields.push(part);
|
|
160
|
+
}
|
|
161
|
+
return fields;
|
|
162
|
+
}
|
|
163
|
+
function scopesFor(flags, profile, defaults, help) {
|
|
164
|
+
if (profile.mode === "application") {
|
|
165
|
+
if (flags.scopes !== undefined) {
|
|
166
|
+
throw new AxiError("Application profiles use the configured Graph .default audience; delegated scopes are unavailable", "VALIDATION_ERROR", [help, "mg-axi profile show --profile <name>"]);
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
if (flags.scopes === undefined)
|
|
171
|
+
return [...defaults];
|
|
172
|
+
const scopes = String(flags.scopes)
|
|
173
|
+
.split(",")
|
|
174
|
+
.map(scope => scope.trim())
|
|
175
|
+
.filter(scope => scope.length > 0);
|
|
176
|
+
return scopes;
|
|
177
|
+
}
|
|
178
|
+
// Request justification is free text and scheduleInfo/ticketInfo are nested
|
|
179
|
+
// objects, so truncation walks values recursively; --full returns them whole.
|
|
180
|
+
function truncateValue(value, full) {
|
|
181
|
+
if (full)
|
|
182
|
+
return { value, truncated: false };
|
|
183
|
+
if (typeof value === "string" && value.length > TRUNCATE_AT) {
|
|
184
|
+
return { value: `${value.slice(0, TRUNCATE_AT)}... (truncated, ${value.length} chars total)`, truncated: true };
|
|
185
|
+
}
|
|
186
|
+
if (Array.isArray(value)) {
|
|
187
|
+
const results = value.map(item => truncateValue(item, full));
|
|
188
|
+
return { value: results.map(result => result.value), truncated: results.some(result => result.truncated) };
|
|
189
|
+
}
|
|
190
|
+
if (value !== null && typeof value === "object") {
|
|
191
|
+
let truncated = false;
|
|
192
|
+
const entries = Object.entries(value).map(([key, item]) => {
|
|
193
|
+
const result = truncateValue(item, full);
|
|
194
|
+
truncated = truncated || result.truncated;
|
|
195
|
+
return [key, result.value];
|
|
196
|
+
});
|
|
197
|
+
return { value: Object.fromEntries(entries), truncated };
|
|
198
|
+
}
|
|
199
|
+
return { value, truncated: false };
|
|
200
|
+
}
|
|
201
|
+
// Local projection preserves Graph's null/missing distinction: an explicit
|
|
202
|
+
// null stays null, an absent property stays absent and is never synthesized.
|
|
203
|
+
function project(row, fields, full) {
|
|
204
|
+
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
205
|
+
const projected = {};
|
|
206
|
+
let truncated = false;
|
|
207
|
+
for (const field of fields) {
|
|
208
|
+
if (!Object.hasOwn(source, field))
|
|
209
|
+
continue;
|
|
210
|
+
const result = truncateValue(source[field], full);
|
|
211
|
+
projected[field] = result.value;
|
|
212
|
+
truncated = truncated || result.truncated;
|
|
213
|
+
}
|
|
214
|
+
return { row: projected, truncated };
|
|
215
|
+
}
|
|
216
|
+
function selectedFields(flags, defaults, known, knownList, help) {
|
|
217
|
+
const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, "select", known, knownList, help);
|
|
218
|
+
const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, "fields", known, knownList, help);
|
|
219
|
+
const missing = fields.find(field => !select.includes(field));
|
|
220
|
+
if (missing) {
|
|
221
|
+
throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
|
|
222
|
+
}
|
|
223
|
+
return { select, fields };
|
|
224
|
+
}
|
|
225
|
+
function profileHint(profileName) {
|
|
226
|
+
return `--profile ${shellValue(profileName)}`;
|
|
227
|
+
}
|
|
228
|
+
function shellValue(value) {
|
|
229
|
+
return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
230
|
+
}
|
|
231
|
+
function fullHint(command, flags, profileName) {
|
|
232
|
+
const args = Object.entries({ ...flags, profile: profileName, full: true })
|
|
233
|
+
.map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
|
|
234
|
+
return `mg-axi ${command} ${args.join(" ")}`;
|
|
235
|
+
}
|
|
236
|
+
// Denials carry the session's generic grant/role/licence cause; each
|
|
237
|
+
// family adds the scope, roles and licensing that actually unlock it,
|
|
238
|
+
// because a 403 alone never says which prerequisite is missing.
|
|
239
|
+
function withGuidance(hints, run) {
|
|
240
|
+
return run().catch(error => {
|
|
241
|
+
if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
|
|
242
|
+
throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const ASSIGNMENT_DENIAL_HINTS = [
|
|
248
|
+
"Group PIM assignment reads need PrivilegedAssignmentSchedule.Read.AzureADGroup plus owner/member of the group or a supported directory role (Global Reader or Privileged Role Administrator for role-assignable groups; Global Reader, Directory Writer, Groups Administrator, Identity Governance Administrator or User Administrator otherwise, scoped at directory level) for delegated access, or admin-consented PrivilegedAssignmentSchedule.Read.AzureADGroup for application access; delegated personal Microsoft accounts are not supported",
|
|
249
|
+
"PIM for Groups needs P2 or ID Governance, not only P2",
|
|
250
|
+
];
|
|
251
|
+
const ELIGIBILITY_DENIAL_HINTS = [
|
|
252
|
+
"Group PIM eligibility reads need PrivilegedEligibilitySchedule.Read.AzureADGroup plus owner/member of the group or a supported directory role (Global Reader or Privileged Role Administrator for role-assignable groups; Global Reader, Directory Writer, Groups Administrator, Identity Governance Administrator or User Administrator otherwise, scoped at directory level) for delegated access, or admin-consented PrivilegedEligibilitySchedule.Read.AzureADGroup for application access; delegated personal Microsoft accounts are not supported",
|
|
253
|
+
"PIM for Groups needs P2 or ID Governance, not only P2",
|
|
254
|
+
];
|
|
255
|
+
const ASSIGNMENT_SCHEDULE_LIST = {
|
|
256
|
+
command: "entra pim group-assignment-schedule list",
|
|
257
|
+
key: "assignmentSchedules",
|
|
258
|
+
noun: "assignment schedules",
|
|
259
|
+
known: ASSIGNMENT_SCHEDULE_KNOWN,
|
|
260
|
+
knownList: KNOWN_ASSIGNMENT_SCHEDULE_FIELDS,
|
|
261
|
+
defaultSelect: DEFAULT_ASSIGNMENT_SCHEDULE_LIST_SELECT,
|
|
262
|
+
defaultScopes: DEFAULT_ASSIGNMENT_SCOPES,
|
|
263
|
+
denialHints: ASSIGNMENT_DENIAL_HINTS,
|
|
264
|
+
scopeNote: "Assignment schedules govern group membership or ownership over time; instances are the provisioned windows, never the schedule itself",
|
|
265
|
+
emptyNote: "0 assignment schedules matched; the absence of results is the answer, not an error",
|
|
266
|
+
};
|
|
267
|
+
const ASSIGNMENT_INSTANCE_LIST = {
|
|
268
|
+
command: "entra pim group-assignment-instance list",
|
|
269
|
+
key: "assignmentScheduleInstances",
|
|
270
|
+
noun: "assignment instances",
|
|
271
|
+
known: ASSIGNMENT_INSTANCE_KNOWN,
|
|
272
|
+
knownList: KNOWN_ASSIGNMENT_INSTANCE_FIELDS,
|
|
273
|
+
defaultSelect: DEFAULT_ASSIGNMENT_INSTANCE_LIST_SELECT,
|
|
274
|
+
defaultScopes: DEFAULT_ASSIGNMENT_SCOPES,
|
|
275
|
+
denialHints: ASSIGNMENT_DENIAL_HINTS,
|
|
276
|
+
scopeNote: "Assignment instances are provisioned membership or ownership windows; assignmentType Assigned names direct grants and Activated names activated eligible grants",
|
|
277
|
+
emptyNote: "0 assignment instances matched; the absence of results is the answer, not an error",
|
|
278
|
+
};
|
|
279
|
+
const ELIGIBILITY_SCHEDULE_LIST = {
|
|
280
|
+
command: "entra pim group-eligibility-schedule list",
|
|
281
|
+
key: "eligibilitySchedules",
|
|
282
|
+
noun: "eligibility schedules",
|
|
283
|
+
known: ELIGIBILITY_SCHEDULE_KNOWN,
|
|
284
|
+
knownList: KNOWN_ELIGIBILITY_SCHEDULE_FIELDS,
|
|
285
|
+
defaultSelect: DEFAULT_ELIGIBILITY_SCHEDULE_LIST_SELECT,
|
|
286
|
+
defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
|
|
287
|
+
denialHints: ELIGIBILITY_DENIAL_HINTS,
|
|
288
|
+
scopeNote: "Eligibility schedules govern who may activate group membership or ownership; they are not active grants",
|
|
289
|
+
emptyNote: "0 eligibility schedules matched; the absence of results is the answer, not an error",
|
|
290
|
+
};
|
|
291
|
+
const ELIGIBILITY_INSTANCE_LIST = {
|
|
292
|
+
command: "entra pim group-eligibility-instance list",
|
|
293
|
+
key: "eligibilityScheduleInstances",
|
|
294
|
+
noun: "eligibility instances",
|
|
295
|
+
known: ELIGIBILITY_INSTANCE_KNOWN,
|
|
296
|
+
knownList: KNOWN_ELIGIBILITY_INSTANCE_FIELDS,
|
|
297
|
+
defaultSelect: DEFAULT_ELIGIBILITY_INSTANCE_LIST_SELECT,
|
|
298
|
+
defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
|
|
299
|
+
denialHints: ELIGIBILITY_DENIAL_HINTS,
|
|
300
|
+
scopeNote: "Eligibility instances are provisioned eligibility windows; activation itself is a PIM workflow outside these reads",
|
|
301
|
+
emptyNote: "0 eligibility instances matched; the absence of results is the answer, not an error",
|
|
302
|
+
};
|
|
303
|
+
const ELIGIBILITY_REQUEST_LIST = {
|
|
304
|
+
command: "entra pim group-eligibility-request list",
|
|
305
|
+
key: "eligibilityScheduleRequests",
|
|
306
|
+
noun: "eligibility requests",
|
|
307
|
+
known: ELIGIBILITY_REQUEST_KNOWN,
|
|
308
|
+
knownList: KNOWN_ELIGIBILITY_REQUEST_FIELDS,
|
|
309
|
+
defaultSelect: DEFAULT_ELIGIBILITY_REQUEST_LIST_SELECT,
|
|
310
|
+
defaultScopes: DEFAULT_ELIGIBILITY_SCOPES,
|
|
311
|
+
denialHints: ELIGIBILITY_DENIAL_HINTS,
|
|
312
|
+
scopeNote: "Eligibility requests carry the ask and its outcome; justification text rides only behind an explicit --select",
|
|
313
|
+
emptyNote: "0 eligibility requests matched; the absence of results is the answer, not an error",
|
|
314
|
+
};
|
|
315
|
+
// Graph requires every one of these lists to be scoped to a groupId or a
|
|
316
|
+
// principalId, so --filter must name one of them; validated before any
|
|
317
|
+
// credential or transport is touched, with cursor resumes replaying the
|
|
318
|
+
// stored filter.
|
|
319
|
+
function requiredFilter(flags, savedFilter, help) {
|
|
320
|
+
const filter = flags.filter === undefined ? savedFilter : String(flags.filter);
|
|
321
|
+
if (filter === undefined || (!filter.includes("groupId") && !filter.includes("principalId"))) {
|
|
322
|
+
throw new AxiError("--filter needs a groupId or principalId eq clause scoping the list", "VALIDATION_ERROR", [
|
|
323
|
+
help,
|
|
324
|
+
"For example --filter \"groupId eq '<group-id>'\" or --filter \"principalId eq '<principal-id>'\"",
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
return filter;
|
|
328
|
+
}
|
|
329
|
+
async function listCollection(shape, session, flags, profile, operation, help, profileName) {
|
|
330
|
+
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
331
|
+
if (cursor !== undefined && !cursor.trim())
|
|
332
|
+
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
333
|
+
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
334
|
+
const savedSelect = saved?.$select;
|
|
335
|
+
const { select, fields } = selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help);
|
|
336
|
+
const filter = requiredFilter(flags, saved?.$filter, help);
|
|
337
|
+
const scopes = scopesFor(flags, profile, shape.defaultScopes, help);
|
|
338
|
+
const full = flags.full === true;
|
|
339
|
+
// These lists document $select/$filter/$expand, so --filter passes
|
|
340
|
+
// through as plain $filter with no $count or ConsistencyLevel attached;
|
|
341
|
+
// $expand stays unreviewed and is never sent.
|
|
342
|
+
const query = { $select: select.join(","), $filter: filter };
|
|
343
|
+
const args = { profile, operation, query, scopes };
|
|
344
|
+
if (cursor !== undefined)
|
|
345
|
+
args.cursor = cursor;
|
|
346
|
+
if (flags.all === true) {
|
|
347
|
+
if (flags.limit !== undefined)
|
|
348
|
+
throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
|
|
352
|
+
}
|
|
353
|
+
const result = await withGuidance(shape.denialHints, () => session.collect(args));
|
|
354
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? shape.defaultSelect.join(",") };
|
|
355
|
+
if (result.query.$filter !== undefined)
|
|
356
|
+
effectiveFlags.filter = result.query.$filter;
|
|
357
|
+
const rows = [];
|
|
358
|
+
let truncated = false;
|
|
359
|
+
for (const row of result.value) {
|
|
360
|
+
const projected = project(row, fields, full);
|
|
361
|
+
rows.push(projected.row);
|
|
362
|
+
truncated = truncated || projected.truncated;
|
|
363
|
+
}
|
|
364
|
+
const truncationHints = truncated ? [fullHint(shape.command, effectiveFlags, profileName)] : [];
|
|
365
|
+
const standing = [shape.scopeNote];
|
|
366
|
+
if (!result.complete) {
|
|
367
|
+
return {
|
|
368
|
+
[shape.key]: rows,
|
|
369
|
+
...listTotals(rows.length, result.total, shape.noun, false),
|
|
370
|
+
complete: false,
|
|
371
|
+
reason: result.reason,
|
|
372
|
+
cursor: result.cursor,
|
|
373
|
+
help: [...truncationHints, `Resume losslessly with the same --filter and flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, ...standing],
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (!rows.length) {
|
|
377
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
|
|
378
|
+
}
|
|
379
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
|
|
380
|
+
}
|
|
381
|
+
async function showOne(key, command, known, knownList, defaultSelect, defaultScopes, denialHints, session, flags, profile, operation, help, profileName, params) {
|
|
382
|
+
const { select, fields } = selectedFields(flags, defaultSelect, known, knownList, help);
|
|
383
|
+
const scopes = scopesFor(flags, profile, defaultScopes, help);
|
|
384
|
+
const full = flags.full === true;
|
|
385
|
+
const raw = await withGuidance(denialHints, () => session.execute({
|
|
386
|
+
profile,
|
|
387
|
+
operation,
|
|
388
|
+
params,
|
|
389
|
+
query: { $select: select.join(",") },
|
|
390
|
+
scopes,
|
|
391
|
+
}));
|
|
392
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
393
|
+
throw new AxiError(`Graph returned a malformed ${key} body`, "GRAPH_ERROR", [
|
|
394
|
+
"Single-object reads carry one object; treat anything else as unknown, not empty",
|
|
395
|
+
]);
|
|
396
|
+
}
|
|
397
|
+
const { row, truncated } = project(raw, fields, full);
|
|
398
|
+
if (truncated)
|
|
399
|
+
return { [key]: row, help: [fullHint(command, flags, profileName)] };
|
|
400
|
+
return { [key]: row };
|
|
401
|
+
}
|
|
402
|
+
export async function listGroupAssignmentSchedules(session, flags, profile, operation, help, profileName) {
|
|
403
|
+
return listCollection(ASSIGNMENT_SCHEDULE_LIST, session, flags, profile, operation, help, profileName);
|
|
404
|
+
}
|
|
405
|
+
export async function showGroupAssignmentSchedule(session, flags, profile, operation, help, profileName) {
|
|
406
|
+
return showOne("assignmentSchedule", "entra pim group-assignment-schedule show", ASSIGNMENT_SCHEDULE_KNOWN, KNOWN_ASSIGNMENT_SCHEDULE_FIELDS, [...KNOWN_ASSIGNMENT_SCHEDULE_FIELDS], DEFAULT_ASSIGNMENT_SCOPES, ASSIGNMENT_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupAssignmentSchedule-id": String(flags.id) });
|
|
407
|
+
}
|
|
408
|
+
export async function listGroupAssignmentInstances(session, flags, profile, operation, help, profileName) {
|
|
409
|
+
return listCollection(ASSIGNMENT_INSTANCE_LIST, session, flags, profile, operation, help, profileName);
|
|
410
|
+
}
|
|
411
|
+
export async function showGroupAssignmentInstance(session, flags, profile, operation, help, profileName) {
|
|
412
|
+
return showOne("assignmentScheduleInstance", "entra pim group-assignment-instance show", ASSIGNMENT_INSTANCE_KNOWN, KNOWN_ASSIGNMENT_INSTANCE_FIELDS, [...KNOWN_ASSIGNMENT_INSTANCE_FIELDS], DEFAULT_ASSIGNMENT_SCOPES, ASSIGNMENT_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupAssignmentScheduleInstance-id": String(flags.id) });
|
|
413
|
+
}
|
|
414
|
+
export async function listGroupEligibilitySchedules(session, flags, profile, operation, help, profileName) {
|
|
415
|
+
return listCollection(ELIGIBILITY_SCHEDULE_LIST, session, flags, profile, operation, help, profileName);
|
|
416
|
+
}
|
|
417
|
+
export async function showGroupEligibilitySchedule(session, flags, profile, operation, help, profileName) {
|
|
418
|
+
return showOne("eligibilitySchedule", "entra pim group-eligibility-schedule show", ELIGIBILITY_SCHEDULE_KNOWN, KNOWN_ELIGIBILITY_SCHEDULE_FIELDS, [...KNOWN_ELIGIBILITY_SCHEDULE_FIELDS], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilitySchedule-id": String(flags.id) });
|
|
419
|
+
}
|
|
420
|
+
export async function listGroupEligibilityInstances(session, flags, profile, operation, help, profileName) {
|
|
421
|
+
return listCollection(ELIGIBILITY_INSTANCE_LIST, session, flags, profile, operation, help, profileName);
|
|
422
|
+
}
|
|
423
|
+
export async function showGroupEligibilityInstance(session, flags, profile, operation, help, profileName) {
|
|
424
|
+
return showOne("eligibilityScheduleInstance", "entra pim group-eligibility-instance show", ELIGIBILITY_INSTANCE_KNOWN, KNOWN_ELIGIBILITY_INSTANCE_FIELDS, [...KNOWN_ELIGIBILITY_INSTANCE_FIELDS], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilityScheduleInstance-id": String(flags.id) });
|
|
425
|
+
}
|
|
426
|
+
export async function listGroupEligibilityRequests(session, flags, profile, operation, help, profileName) {
|
|
427
|
+
return listCollection(ELIGIBILITY_REQUEST_LIST, session, flags, profile, operation, help, profileName);
|
|
428
|
+
}
|
|
429
|
+
export async function showGroupEligibilityRequest(session, flags, profile, operation, help, profileName) {
|
|
430
|
+
return showOne("eligibilityScheduleRequest", "entra pim group-eligibility-request show", ELIGIBILITY_REQUEST_KNOWN, KNOWN_ELIGIBILITY_REQUEST_FIELDS, [...DEFAULT_ELIGIBILITY_REQUEST_SHOW_SELECT], DEFAULT_ELIGIBILITY_SCOPES, ELIGIBILITY_DENIAL_HINTS, session, flags, profile, operation, help, profileName, { "privilegedAccessGroupEligibilityScheduleRequest-id": String(flags.id) });
|
|
431
|
+
}
|
package/dist/entra-groups.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// READ-02: the Entra group read mapping behind `mg-axi entra group list/show`
|
|
3
4
|
// and `mg-axi entra group member list` / `member-of list` (each with an
|
|
4
5
|
// opt-in `--transitive` flat view). Operation construction stays beside its
|
|
@@ -226,23 +227,25 @@ export async function listGroups(session, flags, profile, operation, help, profi
|
|
|
226
227
|
if (!result.complete) {
|
|
227
228
|
return {
|
|
228
229
|
groups,
|
|
229
|
-
|
|
230
|
+
...listTotals(groups.length, result.total, "groups", false),
|
|
231
|
+
complete: false,
|
|
232
|
+
reason: result.reason,
|
|
230
233
|
cursor: result.cursor,
|
|
231
234
|
help: [...truncationHints, resumeHint(profileName), showHint],
|
|
232
235
|
};
|
|
233
236
|
}
|
|
234
|
-
const count = { returned: groups.length, complete: true };
|
|
235
237
|
if (!groups.length) {
|
|
236
238
|
return {
|
|
237
239
|
groups,
|
|
238
|
-
|
|
240
|
+
...listTotals(groups.length, result.total, "groups", true),
|
|
241
|
+
complete: true,
|
|
239
242
|
help: [
|
|
240
243
|
`mg-axi entra group list --filter <odata-filter> ${profileHint(profileName)}`,
|
|
241
244
|
"0 groups matched; the absence of results is the answer, not an error",
|
|
242
245
|
],
|
|
243
246
|
};
|
|
244
247
|
}
|
|
245
|
-
return { groups,
|
|
248
|
+
return { groups, ...listTotals(groups.length, result.total, "groups", true), complete: true, help: [...truncationHints, showHint] };
|
|
246
249
|
}
|
|
247
250
|
export async function showGroup(session, flags, profile, operation, help, profileName) {
|
|
248
251
|
const { select, fields } = selectedFields(flags, KNOWN_GROUPS, KNOWN_GROUP_FIELDS, DEFAULT_SHOW_SELECT, help);
|
|
@@ -302,17 +305,19 @@ async function listRelationship(session, flags, profile, operation, help, profil
|
|
|
302
305
|
if (!result.complete) {
|
|
303
306
|
return {
|
|
304
307
|
[key]: rows,
|
|
305
|
-
|
|
308
|
+
...listTotals(rows.length, result.total, emptyNoun, false),
|
|
309
|
+
complete: false,
|
|
310
|
+
reason: result.reason,
|
|
306
311
|
cursor: result.cursor,
|
|
307
312
|
...warnings,
|
|
308
313
|
help: [...truncationHints, ...limitedHints, hiddenHint, resumeHint(profileName), scopeHint],
|
|
309
314
|
};
|
|
310
315
|
}
|
|
311
|
-
const count = { returned: rows.length, complete: true };
|
|
312
316
|
if (!rows.length) {
|
|
313
317
|
return {
|
|
314
318
|
[key]: rows,
|
|
315
|
-
|
|
319
|
+
...listTotals(rows.length, result.total, emptyNoun, true),
|
|
320
|
+
complete: true,
|
|
316
321
|
...warnings,
|
|
317
322
|
help: [
|
|
318
323
|
...limitedHints,
|
|
@@ -322,7 +327,7 @@ async function listRelationship(session, flags, profile, operation, help, profil
|
|
|
322
327
|
],
|
|
323
328
|
};
|
|
324
329
|
}
|
|
325
|
-
return { [key]: rows,
|
|
330
|
+
return { [key]: rows, ...listTotals(rows.length, result.total, emptyNoun, true), complete: true, ...warnings, help: [...truncationHints, ...limitedHints, hiddenHint, scopeHint] };
|
|
326
331
|
}
|
|
327
332
|
export async function listGroupMembers(session, flags, profile, operation, help, profileName) {
|
|
328
333
|
const transitive = flags.transitive === true;
|