@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.
- package/README.md +251 -77
- package/dist/api.js +253 -8
- package/dist/bin/mg-axi-hook.js +31 -0
- package/dist/catalogue.js +693 -2
- package/dist/cli.js +155 -17
- package/dist/docs.js +175 -46
- package/dist/doctor.js +2 -2
- package/dist/entra-access-reviews.js +11 -4
- package/dist/entra-apps.js +171 -14
- package/dist/entra-audit-logs.js +101 -16
- package/dist/entra-auth-methods.js +8 -4
- package/dist/entra-certificate-auth.js +12 -8
- package/dist/entra-conditional-access.js +425 -15
- 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-federation-configurations.js +7 -4
- package/dist/entra-grants.js +8 -4
- package/dist/entra-group-lifecycle.js +13 -8
- package/dist/entra-group-pim.js +431 -0
- package/dist/entra-groups.js +13 -8
- package/dist/entra-identity-providers.js +7 -4
- package/dist/entra-lifecycle-workflows.js +152 -13
- package/dist/entra-multi-tenant-organization.js +7 -4
- package/dist/entra-on-premises-synchronization.js +6 -4
- package/dist/entra-organization.js +13 -8
- package/dist/entra-risk-prevention.js +442 -0
- package/dist/entra-risk.js +224 -9
- package/dist/entra-roles.js +204 -12
- package/dist/entra-subscriptions.js +6 -4
- package/dist/entra-terms-of-use.js +13 -8
- package/dist/entra-users.js +7 -4
- package/dist/graph-session.js +30 -5
- package/dist/hook.js +84 -0
- package/dist/list-totals.js +52 -0
- package/dist/setup.js +9 -1
- package/docs/coverage.md +743 -610
- package/inventory/operations.json +2 -2
- package/inventory/schema.json +1 -1
- package/package.json +4 -3
- package/skills/mg-axi/SKILL.md +86 -1
package/dist/entra-risk.js
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// READ-06: the Entra risky-user and risk-detection read mapping behind
|
|
3
4
|
// `mg-axi entra risky-user list/show` and `mg-axi entra risk-detection
|
|
4
|
-
// list/show
|
|
5
|
+
// list/show`, extended with the workload-identity surface behind
|
|
6
|
+
// `mg-axi entra risky-service-principal list/show`,
|
|
7
|
+
// `mg-axi entra risky-service-principal history list/show` and
|
|
8
|
+
// `mg-axi entra service-principal-risk-detection list/show`. Operation construction stays beside its command; the shared
|
|
5
9
|
// session owns URLs, credentials, paging, retries and error translation, and
|
|
6
10
|
// the SDK owns TOON rendering. This module only maps flags to session calls,
|
|
7
11
|
// bounds detection queries in time, and projects rows for compact output.
|
|
8
12
|
//
|
|
9
13
|
// Reviewed against the v1.0 riskyuser-list/get and riskdetection-list/get
|
|
10
|
-
// operation documentation on 2026-10-04
|
|
14
|
+
// operation documentation on 2026-10-04, and against the v1.0
|
|
15
|
+
// riskyserviceprincipal-list/get, history-list and
|
|
16
|
+
// serviceprincipalriskdetection-list/get operation documentation on
|
|
17
|
+
// 2026-10-06. Risky-user reads take D/A
|
|
11
18
|
// IdentityRiskyUser.Read.All and risk-detection reads take D/A
|
|
12
|
-
// IdentityRiskEvent.Read.All;
|
|
19
|
+
// IdentityRiskEvent.Read.All; risky-service-principal reads (including
|
|
20
|
+
// history) take D/A IdentityRiskyServicePrincipal.Read.All while
|
|
21
|
+
// service-principal risk detections reuse D/A IdentityRiskEvent.Read.All.
|
|
22
|
+
// Delegated callers additionally need Global
|
|
13
23
|
// Reader, Security Operator, Security Reader or Security Administrator. The
|
|
14
24
|
// riskyUsers API requires a P2 licence, while risk detection permits P1 or
|
|
15
|
-
// P2
|
|
25
|
+
// P2; the workload-identity APIs require a Microsoft Entra Workload
|
|
26
|
+
// Identities Premium licence, and service-principal detections report
|
|
27
|
+
// riskDetail and riskLevel hidden without it. Limited views stay limited: a premium detection without P2 detail
|
|
16
28
|
// reports riskEventType generic, hidden risk levels report the licence
|
|
17
29
|
// boundary instead of the level, and a null detection correlationId means no
|
|
18
30
|
// sign-in is associated. None of these is ever reinterpreted as empty or
|
|
@@ -54,8 +66,67 @@ export const KNOWN_RISK_DETECTION_FIELDS = [
|
|
|
54
66
|
"userPrincipalName",
|
|
55
67
|
"additionalInfo",
|
|
56
68
|
];
|
|
69
|
+
// Workload-identity surface. History items inherit every risky-service-
|
|
70
|
+
// principal property and add the service-principal identifier plus the
|
|
71
|
+
// actor/activity of the risk change. Service-principal detections carry the
|
|
72
|
+
// service-principal join keys (servicePrincipalId, servicePrincipalDisplayName,
|
|
73
|
+
// appId, keyIds) instead of user keys; keyIds are key-credential identifiers,
|
|
74
|
+
// never secret material. tokenIssuerType stays absent for the same redaction
|
|
75
|
+
// reason as above.
|
|
76
|
+
export const KNOWN_RISKY_SP_FIELDS = [
|
|
77
|
+
"id",
|
|
78
|
+
"isEnabled",
|
|
79
|
+
"isProcessing",
|
|
80
|
+
"riskDetail",
|
|
81
|
+
"riskLastUpdatedDateTime",
|
|
82
|
+
"riskLevel",
|
|
83
|
+
"riskState",
|
|
84
|
+
"displayName",
|
|
85
|
+
"appId",
|
|
86
|
+
"servicePrincipalType",
|
|
87
|
+
];
|
|
88
|
+
export const KNOWN_RISKY_SP_HISTORY_FIELDS = [
|
|
89
|
+
"id",
|
|
90
|
+
"isEnabled",
|
|
91
|
+
"isProcessing",
|
|
92
|
+
"riskDetail",
|
|
93
|
+
"riskLastUpdatedDateTime",
|
|
94
|
+
"riskLevel",
|
|
95
|
+
"riskState",
|
|
96
|
+
"displayName",
|
|
97
|
+
"appId",
|
|
98
|
+
"servicePrincipalType",
|
|
99
|
+
"servicePrincipalId",
|
|
100
|
+
"initiatedBy",
|
|
101
|
+
"activity",
|
|
102
|
+
];
|
|
103
|
+
export const KNOWN_SP_DETECTION_FIELDS = [
|
|
104
|
+
"id",
|
|
105
|
+
"requestId",
|
|
106
|
+
"correlationId",
|
|
107
|
+
"riskEventType",
|
|
108
|
+
"riskState",
|
|
109
|
+
"riskLevel",
|
|
110
|
+
"riskDetail",
|
|
111
|
+
"source",
|
|
112
|
+
"detectionTimingType",
|
|
113
|
+
"activity",
|
|
114
|
+
"ipAddress",
|
|
115
|
+
"location",
|
|
116
|
+
"activityDateTime",
|
|
117
|
+
"detectedDateTime",
|
|
118
|
+
"lastUpdatedDateTime",
|
|
119
|
+
"servicePrincipalId",
|
|
120
|
+
"servicePrincipalDisplayName",
|
|
121
|
+
"appId",
|
|
122
|
+
"keyIds",
|
|
123
|
+
"additionalInfo",
|
|
124
|
+
];
|
|
57
125
|
const RISKY_USER_KNOWN = new Set(KNOWN_RISKY_USER_FIELDS);
|
|
58
126
|
const RISK_DETECTION_KNOWN = new Set(KNOWN_RISK_DETECTION_FIELDS);
|
|
127
|
+
const RISKY_SP_KNOWN = new Set(KNOWN_RISKY_SP_FIELDS);
|
|
128
|
+
const RISKY_SP_HISTORY_KNOWN = new Set(KNOWN_RISKY_SP_HISTORY_FIELDS);
|
|
129
|
+
const SP_DETECTION_KNOWN = new Set(KNOWN_SP_DETECTION_FIELDS);
|
|
59
130
|
// Compact list rows: identifier, who is at risk, and the triage state.
|
|
60
131
|
const DEFAULT_RISKY_USER_LIST_SELECT = ["id", "userPrincipalName", "riskLevel", "riskState"];
|
|
61
132
|
const DEFAULT_RISK_DETECTION_LIST_SELECT = ["id", "detectedDateTime", "userPrincipalName", "riskLevel"];
|
|
@@ -64,6 +135,7 @@ const DEFAULT_RISKY_USER_SHOW_SELECT = [...KNOWN_RISKY_USER_FIELDS];
|
|
|
64
135
|
const DEFAULT_RISK_DETECTION_SHOW_SELECT = [...KNOWN_RISK_DETECTION_FIELDS];
|
|
65
136
|
export const DEFAULT_RISKY_USER_SCOPES = ["https://graph.microsoft.com/IdentityRiskyUser.Read.All"];
|
|
66
137
|
export const DEFAULT_RISK_DETECTION_SCOPES = ["https://graph.microsoft.com/IdentityRiskEvent.Read.All"];
|
|
138
|
+
export const DEFAULT_RISKY_SP_SCOPES = ["https://graph.microsoft.com/IdentityRiskyServicePrincipal.Read.All"];
|
|
67
139
|
const TRUNCATE_AT = 500;
|
|
68
140
|
function fieldList(raw, flag, known, knownList, help) {
|
|
69
141
|
const parts = String(raw)
|
|
@@ -222,9 +294,27 @@ const RISK_DETECTION_DENIAL_HINTS = [
|
|
|
222
294
|
"Risk-detection reads need IdentityRiskEvent.Read.All plus a supported directory role: Global Reader, Security Operator, Security Reader or Security Administrator for delegated access, or admin-consented IdentityRiskEvent.Read.All for application access",
|
|
223
295
|
"Risk detection needs a P1 or P2 licence; premium detections report riskEventType generic without P2 detail, so generic is the server's limited view rather than the real type",
|
|
224
296
|
];
|
|
297
|
+
const RISKY_SP_DENIAL_HINTS = [
|
|
298
|
+
"Risky-service-principal reads need IdentityRiskyServicePrincipal.Read.All plus a supported directory role: Global Reader, Security Operator, Security Reader or Security Administrator for delegated access, or admin-consented IdentityRiskyServicePrincipal.Read.All for application access",
|
|
299
|
+
"The riskyServicePrincipals API requires a Microsoft Entra Workload Identities Premium licence; data availability follows Entra retention policies",
|
|
300
|
+
];
|
|
301
|
+
const SP_DETECTION_DENIAL_HINTS = [
|
|
302
|
+
"Service-principal-risk-detection reads need IdentityRiskEvent.Read.All plus a supported directory role: Global Reader, Security Operator, Security Reader or Security Administrator for delegated access, or admin-consented IdentityRiskEvent.Read.All for application access",
|
|
303
|
+
"The servicePrincipalRiskDetection API requires a Microsoft Entra Workload Identities Premium licence; riskDetail and riskLevel report hidden without it, so hidden is the server's limited view rather than no risk",
|
|
304
|
+
];
|
|
305
|
+
// Nested history routes bind the parent risky-service-principal id before
|
|
306
|
+
// credentials; a missing or empty parent fails as usage, never as Graph 404.
|
|
307
|
+
function parentServicePrincipal(flags, help) {
|
|
308
|
+
const parent = flags["service-principal"];
|
|
309
|
+
if (parent === undefined || !String(parent).trim()) {
|
|
310
|
+
throw new AxiError("--service-principal needs the risky-service-principal object ID owning this history", "VALIDATION_ERROR", [help]);
|
|
311
|
+
}
|
|
312
|
+
return { "riskyServicePrincipal-id": String(parent) };
|
|
313
|
+
}
|
|
225
314
|
const RISKY_USER_LIST = {
|
|
226
315
|
noun: "risky-user",
|
|
227
316
|
key: "riskyUsers",
|
|
317
|
+
totalsNoun: "risky users",
|
|
228
318
|
known: RISKY_USER_KNOWN,
|
|
229
319
|
knownList: KNOWN_RISKY_USER_FIELDS,
|
|
230
320
|
defaultSelect: DEFAULT_RISKY_USER_LIST_SELECT,
|
|
@@ -241,6 +331,7 @@ const RISKY_USER_LIST = {
|
|
|
241
331
|
const RISK_DETECTION_LIST = {
|
|
242
332
|
noun: "risk-detection",
|
|
243
333
|
key: "riskDetections",
|
|
334
|
+
totalsNoun: "risk detections",
|
|
244
335
|
dateField: "detectedDateTime",
|
|
245
336
|
known: RISK_DETECTION_KNOWN,
|
|
246
337
|
knownList: KNOWN_RISK_DETECTION_FIELDS,
|
|
@@ -257,6 +348,65 @@ const RISK_DETECTION_LIST = {
|
|
|
257
348
|
"Limited views stay limited: riskEventType generic can hide a premium detection without P2 detail, and hidden risk levels report the licence boundary instead of the level - never read them as none",
|
|
258
349
|
],
|
|
259
350
|
};
|
|
351
|
+
// Compact workload rows: identifier, which workload, and the triage state.
|
|
352
|
+
const DEFAULT_RISKY_SP_LIST_SELECT = ["id", "displayName", "riskLevel", "riskState"];
|
|
353
|
+
const DEFAULT_SP_DETECTION_LIST_SELECT = ["id", "detectedDateTime", "servicePrincipalDisplayName", "riskLevel"];
|
|
354
|
+
const DEFAULT_RISKY_SP_SHOW_SELECT = [...KNOWN_RISKY_SP_FIELDS];
|
|
355
|
+
const DEFAULT_RISKY_SP_HISTORY_SHOW_SELECT = [...KNOWN_RISKY_SP_HISTORY_FIELDS];
|
|
356
|
+
const DEFAULT_SP_DETECTION_SHOW_SELECT = [...KNOWN_SP_DETECTION_FIELDS];
|
|
357
|
+
const RISKY_SP_LIST = {
|
|
358
|
+
noun: "risky-service-principal",
|
|
359
|
+
key: "riskyServicePrincipals",
|
|
360
|
+
totalsNoun: "risky service principals",
|
|
361
|
+
known: RISKY_SP_KNOWN,
|
|
362
|
+
knownList: KNOWN_RISKY_SP_FIELDS,
|
|
363
|
+
defaultSelect: DEFAULT_RISKY_SP_LIST_SELECT,
|
|
364
|
+
defaultScopes: DEFAULT_RISKY_SP_SCOPES,
|
|
365
|
+
denialHints: RISKY_SP_DENIAL_HINTS,
|
|
366
|
+
showHint: "mg-axi entra risky-service-principal show --id <risky-service-principal-id>",
|
|
367
|
+
emptyHints: profileName => [
|
|
368
|
+
`mg-axi entra risky-service-principal list --filter <odata-filter> ${profileHint(profileName)}`,
|
|
369
|
+
"0 risky service principals matched; the absence of results is the answer, not an error",
|
|
370
|
+
"Limited results stay limited: without Workload Identities Premium detail the service reports what the caller may see, so never read a short list as no risk",
|
|
371
|
+
],
|
|
372
|
+
extraHelp: () => [],
|
|
373
|
+
};
|
|
374
|
+
const RISKY_SP_HISTORY_LIST = {
|
|
375
|
+
noun: "risky-service-principal history",
|
|
376
|
+
key: "riskyServicePrincipalHistory",
|
|
377
|
+
totalsNoun: "history items",
|
|
378
|
+
pathParams: parentServicePrincipal,
|
|
379
|
+
known: RISKY_SP_HISTORY_KNOWN,
|
|
380
|
+
knownList: KNOWN_RISKY_SP_HISTORY_FIELDS,
|
|
381
|
+
defaultSelect: DEFAULT_RISKY_SP_LIST_SELECT,
|
|
382
|
+
defaultScopes: DEFAULT_RISKY_SP_SCOPES,
|
|
383
|
+
denialHints: RISKY_SP_DENIAL_HINTS,
|
|
384
|
+
showHint: "mg-axi entra risky-service-principal history show --service-principal <risky-service-principal-id> --id <history-item-id>",
|
|
385
|
+
emptyHints: profileName => [
|
|
386
|
+
`mg-axi entra risky-service-principal history list --service-principal <risky-service-principal-id> ${profileHint(profileName)}`,
|
|
387
|
+
"0 history items matched; the absence of results is the answer, not an error",
|
|
388
|
+
],
|
|
389
|
+
extraHelp: () => [],
|
|
390
|
+
};
|
|
391
|
+
const SP_DETECTION_LIST = {
|
|
392
|
+
noun: "service-principal-risk-detection",
|
|
393
|
+
key: "servicePrincipalRiskDetections",
|
|
394
|
+
totalsNoun: "service principal risk detections",
|
|
395
|
+
dateField: "detectedDateTime",
|
|
396
|
+
known: SP_DETECTION_KNOWN,
|
|
397
|
+
knownList: KNOWN_SP_DETECTION_FIELDS,
|
|
398
|
+
defaultSelect: DEFAULT_SP_DETECTION_LIST_SELECT,
|
|
399
|
+
defaultScopes: DEFAULT_RISK_DETECTION_SCOPES,
|
|
400
|
+
denialHints: SP_DETECTION_DENIAL_HINTS,
|
|
401
|
+
showHint: "mg-axi entra service-principal-risk-detection show --id <service-principal-risk-detection-id>",
|
|
402
|
+
emptyHints: profileName => [
|
|
403
|
+
`mg-axi entra service-principal-risk-detection list --since <earlier-iso-time> ${profileHint(profileName)}`,
|
|
404
|
+
"0 service-principal risk detections matched in this window; widen --since/--until or loosen --filter - the absence of results is the answer, not an error",
|
|
405
|
+
],
|
|
406
|
+
extraHelp: () => [
|
|
407
|
+
"Limited views stay limited: riskDetail and riskLevel report hidden without Workload Identities Premium detail - never read hidden as none",
|
|
408
|
+
],
|
|
409
|
+
};
|
|
260
410
|
async function listRisk(shape, session, flags, profile, operation, help, profileName) {
|
|
261
411
|
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
262
412
|
if (cursor !== undefined && !cursor.trim())
|
|
@@ -292,6 +442,8 @@ async function listRisk(shape, session, flags, profile, operation, help, profile
|
|
|
292
442
|
query.$filter = String(flags.filter);
|
|
293
443
|
}
|
|
294
444
|
const args = { profile, operation, query, scopes };
|
|
445
|
+
if (shape.pathParams !== undefined)
|
|
446
|
+
args.params = shape.pathParams(flags, help);
|
|
295
447
|
if (cursor !== undefined)
|
|
296
448
|
args.cursor = cursor;
|
|
297
449
|
if (flags.all === true) {
|
|
@@ -322,16 +474,17 @@ async function listRisk(shape, session, flags, profile, operation, help, profile
|
|
|
322
474
|
if (!result.complete) {
|
|
323
475
|
return {
|
|
324
476
|
[shape.key]: rows,
|
|
325
|
-
|
|
477
|
+
...listTotals(rows.length, result.total, shape.totalsNoun, false),
|
|
478
|
+
complete: false,
|
|
479
|
+
reason: result.reason,
|
|
326
480
|
cursor: result.cursor,
|
|
327
481
|
help: [...truncationHints, `Resume losslessly with the same flags plus --cursor - ${profileHint(profileName)} and supply the returned cursor on stdin`, showHint, ...shape.extraHelp(profileName)],
|
|
328
482
|
};
|
|
329
483
|
}
|
|
330
|
-
const count = { returned: rows.length, complete: true };
|
|
331
484
|
if (!rows.length) {
|
|
332
|
-
return { [shape.key]: rows,
|
|
485
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.totalsNoun, true), complete: true, help: shape.emptyHints(profileName) };
|
|
333
486
|
}
|
|
334
|
-
return { [shape.key]: rows,
|
|
487
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.totalsNoun, true), complete: true, help: [...truncationHints, showHint, ...shape.extraHelp(profileName)] };
|
|
335
488
|
}
|
|
336
489
|
const RISKY_USER_SHOW = {
|
|
337
490
|
noun: "risky-user",
|
|
@@ -363,6 +516,50 @@ const RISK_DETECTION_SHOW = {
|
|
|
363
516
|
],
|
|
364
517
|
noSignInNote: "This detection carries no associated sign-in (correlationId is null); sign-in correlation does not apply",
|
|
365
518
|
};
|
|
519
|
+
const RISKY_SP_SHOW = {
|
|
520
|
+
noun: "risky-service-principal",
|
|
521
|
+
key: "riskyServicePrincipal",
|
|
522
|
+
param: "riskyServicePrincipal-id",
|
|
523
|
+
known: RISKY_SP_KNOWN,
|
|
524
|
+
knownList: KNOWN_RISKY_SP_FIELDS,
|
|
525
|
+
defaultSelect: DEFAULT_RISKY_SP_SHOW_SELECT,
|
|
526
|
+
defaultScopes: DEFAULT_RISKY_SP_SCOPES,
|
|
527
|
+
denialHints: RISKY_SP_DENIAL_HINTS,
|
|
528
|
+
correlationHelp: profileName => [
|
|
529
|
+
`List this workload's detections with mg-axi entra service-principal-risk-detection list --since <iso-time> --filter "servicePrincipalId eq '<risky-service-principal-id>'" ${profileHint(profileName)}`,
|
|
530
|
+
`List this workload's risk history with mg-axi entra risky-service-principal history list --service-principal <risky-service-principal-id> ${profileHint(profileName)}`,
|
|
531
|
+
"Confirming compromise and dismissing risk are separately reviewed writes; this read never confirms, dismisses or remediates risk",
|
|
532
|
+
],
|
|
533
|
+
};
|
|
534
|
+
const RISKY_SP_HISTORY_SHOW = {
|
|
535
|
+
noun: "risky-service-principal history",
|
|
536
|
+
key: "riskyServicePrincipalHistoryItem",
|
|
537
|
+
param: "riskyServicePrincipalHistoryItem-id",
|
|
538
|
+
parentParams: parentServicePrincipal,
|
|
539
|
+
known: RISKY_SP_HISTORY_KNOWN,
|
|
540
|
+
knownList: KNOWN_RISKY_SP_HISTORY_FIELDS,
|
|
541
|
+
defaultSelect: DEFAULT_RISKY_SP_HISTORY_SHOW_SELECT,
|
|
542
|
+
defaultScopes: DEFAULT_RISKY_SP_SCOPES,
|
|
543
|
+
denialHints: RISKY_SP_DENIAL_HINTS,
|
|
544
|
+
correlationHelp: profileName => [
|
|
545
|
+
`List this workload's detections with mg-axi entra service-principal-risk-detection list --since <iso-time> --filter "servicePrincipalId eq '<risky-service-principal-id>'" ${profileHint(profileName)}`,
|
|
546
|
+
],
|
|
547
|
+
};
|
|
548
|
+
const SP_DETECTION_SHOW = {
|
|
549
|
+
noun: "service-principal-risk-detection",
|
|
550
|
+
key: "servicePrincipalRiskDetection",
|
|
551
|
+
param: "servicePrincipalRiskDetection-id",
|
|
552
|
+
known: SP_DETECTION_KNOWN,
|
|
553
|
+
knownList: KNOWN_SP_DETECTION_FIELDS,
|
|
554
|
+
defaultSelect: DEFAULT_SP_DETECTION_SHOW_SELECT,
|
|
555
|
+
defaultScopes: DEFAULT_RISK_DETECTION_SCOPES,
|
|
556
|
+
denialHints: SP_DETECTION_DENIAL_HINTS,
|
|
557
|
+
correlationHelp: profileName => [
|
|
558
|
+
`List this workload's detections with mg-axi entra service-principal-risk-detection list --since <iso-time> --filter "servicePrincipalId eq '<service-principal-id>'" ${profileHint(profileName)}`,
|
|
559
|
+
"Limited views stay limited: riskDetail and riskLevel report hidden without Workload Identities Premium detail - never read hidden as none",
|
|
560
|
+
],
|
|
561
|
+
noSignInNote: "This detection carries no associated sign-in (correlationId is null); sign-in correlation does not apply",
|
|
562
|
+
};
|
|
366
563
|
async function showRisk(shape, session, flags, profile, operation, help, profileName) {
|
|
367
564
|
const { select, fields } = selectedFields(flags, shape.defaultSelect, shape.known, shape.knownList, help);
|
|
368
565
|
const scopes = scopesFor(flags, profile, shape.defaultScopes, help);
|
|
@@ -370,7 +567,7 @@ async function showRisk(shape, session, flags, profile, operation, help, profile
|
|
|
370
567
|
const raw = await withGuidance(shape.denialHints, () => session.execute({
|
|
371
568
|
profile,
|
|
372
569
|
operation,
|
|
373
|
-
params: { [shape.param]: String(flags.id) },
|
|
570
|
+
params: { ...(shape.parentParams === undefined ? {} : shape.parentParams(flags, help)), [shape.param]: String(flags.id) },
|
|
374
571
|
query: { $select: select.join(",") },
|
|
375
572
|
scopes,
|
|
376
573
|
}));
|
|
@@ -402,3 +599,21 @@ export async function listRiskDetections(session, flags, profile, operation, hel
|
|
|
402
599
|
export async function showRiskDetection(session, flags, profile, operation, help, profileName) {
|
|
403
600
|
return showRisk(RISK_DETECTION_SHOW, session, flags, profile, operation, help, profileName);
|
|
404
601
|
}
|
|
602
|
+
export async function listRiskyServicePrincipals(session, flags, profile, operation, help, profileName) {
|
|
603
|
+
return listRisk(RISKY_SP_LIST, session, flags, profile, operation, help, profileName);
|
|
604
|
+
}
|
|
605
|
+
export async function showRiskyServicePrincipal(session, flags, profile, operation, help, profileName) {
|
|
606
|
+
return showRisk(RISKY_SP_SHOW, session, flags, profile, operation, help, profileName);
|
|
607
|
+
}
|
|
608
|
+
export async function listRiskyServicePrincipalHistory(session, flags, profile, operation, help, profileName) {
|
|
609
|
+
return listRisk(RISKY_SP_HISTORY_LIST, session, flags, profile, operation, help, profileName);
|
|
610
|
+
}
|
|
611
|
+
export async function showRiskyServicePrincipalHistory(session, flags, profile, operation, help, profileName) {
|
|
612
|
+
return showRisk(RISKY_SP_HISTORY_SHOW, session, flags, profile, operation, help, profileName);
|
|
613
|
+
}
|
|
614
|
+
export async function listServicePrincipalRiskDetections(session, flags, profile, operation, help, profileName) {
|
|
615
|
+
return listRisk(SP_DETECTION_LIST, session, flags, profile, operation, help, profileName);
|
|
616
|
+
}
|
|
617
|
+
export async function showServicePrincipalRiskDetection(session, flags, profile, operation, help, profileName) {
|
|
618
|
+
return showRisk(SP_DETECTION_SHOW, session, flags, profile, operation, help, profileName);
|
|
619
|
+
}
|
package/dist/entra-roles.js
CHANGED
|
@@ -1,26 +1,41 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// READ-09: the Entra directory-role and PIM read mapping behind
|
|
3
|
-
// `mg-axi entra directory-role list/show`, `mg-axi entra role-
|
|
4
|
-
// list`, `mg-axi entra
|
|
4
|
+
// `mg-axi entra directory-role list/show`, `mg-axi entra directory-role-template
|
|
5
|
+
// list/show`, `mg-axi entra directory-role member list`, `mg-axi entra
|
|
6
|
+
// directory-role scoped-member list/show`, `mg-axi entra scoped-role-membership
|
|
7
|
+
// list/show`, `mg-axi entra role-assignment list`, `mg-axi entra pim eligible
|
|
8
|
+
// list` and `mg-axi entra pim active list`.
|
|
5
9
|
// Operation construction stays beside its command; the shared session owns
|
|
6
10
|
// URLs, credentials, paging, retries and error translation, and the SDK owns
|
|
7
11
|
// TOON rendering. This module only maps flags to session calls and projects
|
|
8
12
|
// rows for compact output.
|
|
9
13
|
//
|
|
10
14
|
// Reviewed against the v1.0 directoryrole-list, directoryrole-get,
|
|
15
|
+
// directoryroletemplate-list, directoryroletemplate-get,
|
|
16
|
+
// directoryrole-list-members, directoryrole-list-scopedmembers,
|
|
17
|
+
// scopedrolemembership-list, scopedrolemembership-get,
|
|
11
18
|
// rbacapplication-list-roleassignments,
|
|
12
19
|
// rbacapplication-list-roleeligibilityscheduleinstances and
|
|
13
20
|
// rbacapplication-list-roleassignmentscheduleinstances operation
|
|
14
|
-
// documentation on 2026-10-04
|
|
15
|
-
//
|
|
21
|
+
// documentation on 2026-10-04 (roles, assignments and PIM) and 2026-10-06
|
|
22
|
+
// (templates, members, scoped members and scoped memberships). Directory-role reads, role
|
|
23
|
+
// assignments, templates, members, scoped members and scoped memberships
|
|
24
|
+
// read D/A RoleManagement.Read.Directory; eligible PIM reads
|
|
16
25
|
// D/A RoleEligibilitySchedule.Read.Directory; active PIM reads D/A
|
|
17
26
|
// RoleAssignmentSchedule.Read.Directory. Delegated callers additionally need
|
|
18
27
|
// a supported administrator role per operation. None of these collections
|
|
19
28
|
// documents an advanced-query contract, so --filter passes through as plain
|
|
20
29
|
// $filter with no $count or ConsistencyLevel attached.
|
|
21
30
|
//
|
|
22
|
-
// The
|
|
31
|
+
// The views cover: directoryRoles are activated role
|
|
23
32
|
// instances only (a role appears here after activation, never before);
|
|
33
|
+
// directoryRoleTemplates describe every role definition whether activated or
|
|
34
|
+
// not; members are the principals assigned to one activated role (@odata.type
|
|
35
|
+
// names each member kind: user, group, device or service principal); scoped
|
|
36
|
+
// members bind one role to one principal within one directory scope;
|
|
37
|
+
// scopedRoleMemberships are the administrative-unit-scoped admin assignments
|
|
38
|
+
// (directory-wide assignments live on roleAssignments);
|
|
24
39
|
// roleAssignments include direct and PIM-activated assignments; eligible schedule
|
|
25
40
|
// instances are PIM-eligible but not active; active schedule instances cover
|
|
26
41
|
// both directly assigned (assignmentType Assigned) and activated eligible
|
|
@@ -56,10 +71,31 @@ export const KNOWN_ELIGIBLE_FIELDS = [
|
|
|
56
71
|
"endDateTime",
|
|
57
72
|
];
|
|
58
73
|
export const KNOWN_ACTIVE_FIELDS = [...KNOWN_ELIGIBLE_FIELDS, "assignmentType"];
|
|
74
|
+
export const KNOWN_TEMPLATE_FIELDS = [
|
|
75
|
+
"id",
|
|
76
|
+
"displayName",
|
|
77
|
+
"description",
|
|
78
|
+
];
|
|
79
|
+
export const KNOWN_ROLE_MEMBER_FIELDS = [
|
|
80
|
+
"id",
|
|
81
|
+
"displayName",
|
|
82
|
+
"mail",
|
|
83
|
+
"userPrincipalName",
|
|
84
|
+
];
|
|
85
|
+
export const KNOWN_SCOPED_MEMBER_FIELDS = [
|
|
86
|
+
"id",
|
|
87
|
+
"principalId",
|
|
88
|
+
"roleId",
|
|
89
|
+
"directoryScopeId",
|
|
90
|
+
"administrativeUnitId",
|
|
91
|
+
];
|
|
59
92
|
const ROLE_KNOWN = new Set(KNOWN_ROLE_FIELDS);
|
|
60
93
|
const ASSIGNMENT_KNOWN = new Set(KNOWN_ASSIGNMENT_FIELDS);
|
|
61
94
|
const ELIGIBLE_KNOWN = new Set(KNOWN_ELIGIBLE_FIELDS);
|
|
62
95
|
const ACTIVE_KNOWN = new Set(KNOWN_ACTIVE_FIELDS);
|
|
96
|
+
const TEMPLATE_KNOWN = new Set(KNOWN_TEMPLATE_FIELDS);
|
|
97
|
+
const MEMBER_KNOWN = new Set(KNOWN_ROLE_MEMBER_FIELDS);
|
|
98
|
+
const SCOPED_KNOWN = new Set(KNOWN_SCOPED_MEMBER_FIELDS);
|
|
63
99
|
// Compact rows: identifiers plus the correlation keys. For built-in roles
|
|
64
100
|
// the unified roleDefinitionId matches the directory-role roleTemplateId.
|
|
65
101
|
const DEFAULT_ROLE_LIST_SELECT = ["id", "displayName", "description", "roleTemplateId"];
|
|
@@ -67,6 +103,11 @@ const DEFAULT_ROLE_SHOW_SELECT = [...KNOWN_ROLE_FIELDS];
|
|
|
67
103
|
const DEFAULT_ASSIGNMENT_SELECT = ["id", "principalId", "roleDefinitionId", "directoryScopeId"];
|
|
68
104
|
const DEFAULT_ELIGIBLE_SELECT = ["id", "principalId", "roleDefinitionId", "memberType"];
|
|
69
105
|
const DEFAULT_ACTIVE_SELECT = ["id", "principalId", "roleDefinitionId", "assignmentType", "memberType"];
|
|
106
|
+
const DEFAULT_TEMPLATE_LIST_SELECT = ["id", "displayName", "description"];
|
|
107
|
+
const DEFAULT_TEMPLATE_SHOW_SELECT = [...KNOWN_TEMPLATE_FIELDS];
|
|
108
|
+
const DEFAULT_MEMBER_SELECT = ["id", "displayName"];
|
|
109
|
+
const DEFAULT_SCOPED_LIST_SELECT = ["id", "principalId", "roleId", "directoryScopeId"];
|
|
110
|
+
const DEFAULT_SCOPED_SHOW_SELECT = [...KNOWN_SCOPED_MEMBER_FIELDS];
|
|
70
111
|
export const DEFAULT_ROLE_SCOPES = ["https://graph.microsoft.com/RoleManagement.Read.Directory"];
|
|
71
112
|
export const DEFAULT_ELIGIBLE_SCOPES = ["https://graph.microsoft.com/RoleEligibilitySchedule.Read.Directory"];
|
|
72
113
|
export const DEFAULT_ACTIVE_SCOPES = ["https://graph.microsoft.com/RoleAssignmentSchedule.Read.Directory"];
|
|
@@ -114,9 +155,14 @@ function truncateValue(value, full) {
|
|
|
114
155
|
}
|
|
115
156
|
// Local projection preserves Graph's null/missing distinction: an explicit
|
|
116
157
|
// null stays null, an absent property stays absent and is never synthesized.
|
|
117
|
-
function project(row, fields, full) {
|
|
158
|
+
function project(row, fields, full, keepODataType = false) {
|
|
118
159
|
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
119
160
|
const projected = {};
|
|
161
|
+
// Heterogeneous member rows carry their kind in @odata.type, which is
|
|
162
|
+
// structural rather than a selectable property, so it rides along first.
|
|
163
|
+
if (keepODataType && typeof source["@odata.type"] === "string") {
|
|
164
|
+
projected["@odata.type"] = source["@odata.type"];
|
|
165
|
+
}
|
|
120
166
|
let truncated = false;
|
|
121
167
|
for (const field of fields) {
|
|
122
168
|
if (!Object.hasOwn(source, field))
|
|
@@ -174,9 +220,14 @@ const ACTIVE_DENIAL_HINTS = [
|
|
|
174
220
|
"Active PIM reads need RoleAssignmentSchedule.Read.Directory plus Global Reader, Security Operator, Security Reader, Security Administrator or Privileged Role Administrator for delegated access, or admin-consented RoleAssignmentSchedule.Read.Directory for application access",
|
|
175
221
|
"PIM reads need P2 or ID Governance, not only P2",
|
|
176
222
|
];
|
|
223
|
+
const SCOPED_DENIAL_HINTS = [
|
|
224
|
+
"Scoped membership reads need RoleManagement.Read.Directory plus Global Reader, Security Reader or Privileged Role Administrator for delegated access, or admin-consented RoleManagement.Read.Directory for application access",
|
|
225
|
+
"Administrative-unit-scoped memberships need P1",
|
|
226
|
+
];
|
|
177
227
|
const ROLE_LIST = {
|
|
178
228
|
command: "entra directory-role list",
|
|
179
229
|
key: "directoryRoles",
|
|
230
|
+
noun: "directory roles",
|
|
180
231
|
known: ROLE_KNOWN,
|
|
181
232
|
knownList: KNOWN_ROLE_FIELDS,
|
|
182
233
|
defaultSelect: DEFAULT_ROLE_LIST_SELECT,
|
|
@@ -188,6 +239,7 @@ const ROLE_LIST = {
|
|
|
188
239
|
const ASSIGNMENT_LIST = {
|
|
189
240
|
command: "entra role-assignment list",
|
|
190
241
|
key: "roleAssignments",
|
|
242
|
+
noun: "role assignments",
|
|
191
243
|
known: ASSIGNMENT_KNOWN,
|
|
192
244
|
knownList: KNOWN_ASSIGNMENT_FIELDS,
|
|
193
245
|
defaultSelect: DEFAULT_ASSIGNMENT_SELECT,
|
|
@@ -199,6 +251,7 @@ const ASSIGNMENT_LIST = {
|
|
|
199
251
|
const PIM_ELIGIBLE = {
|
|
200
252
|
command: "entra pim eligible list",
|
|
201
253
|
key: "eligibleAssignments",
|
|
254
|
+
noun: "eligible assignments",
|
|
202
255
|
known: ELIGIBLE_KNOWN,
|
|
203
256
|
knownList: KNOWN_ELIGIBLE_FIELDS,
|
|
204
257
|
defaultSelect: DEFAULT_ELIGIBLE_SELECT,
|
|
@@ -210,6 +263,7 @@ const PIM_ELIGIBLE = {
|
|
|
210
263
|
const PIM_ACTIVE = {
|
|
211
264
|
command: "entra pim active list",
|
|
212
265
|
key: "activeAssignments",
|
|
266
|
+
noun: "active assignments",
|
|
213
267
|
known: ACTIVE_KNOWN,
|
|
214
268
|
knownList: KNOWN_ACTIVE_FIELDS,
|
|
215
269
|
defaultSelect: DEFAULT_ACTIVE_SELECT,
|
|
@@ -218,10 +272,68 @@ const PIM_ACTIVE = {
|
|
|
218
272
|
scopeNote: "Active distinguishes directly assigned (assignmentType Assigned) and activated eligible (assignmentType Activated) assignments",
|
|
219
273
|
emptyNote: "0 active assignments matched; the absence of results is the answer, not an error",
|
|
220
274
|
};
|
|
275
|
+
const TEMPLATE_LIST = {
|
|
276
|
+
command: "entra directory-role-template list",
|
|
277
|
+
key: "directoryRoleTemplates",
|
|
278
|
+
noun: "directory-role templates",
|
|
279
|
+
known: TEMPLATE_KNOWN,
|
|
280
|
+
knownList: KNOWN_TEMPLATE_FIELDS,
|
|
281
|
+
defaultSelect: DEFAULT_TEMPLATE_LIST_SELECT,
|
|
282
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
283
|
+
denialHints: ROLE_DENIAL_HINTS,
|
|
284
|
+
scopeNote: "Templates describe every role definition; activation state lives on directoryRoles, which appear only after activation",
|
|
285
|
+
emptyNote: "0 directory-role templates matched; the absence of results is the answer, not an error",
|
|
286
|
+
};
|
|
287
|
+
const MEMBER_LIST = {
|
|
288
|
+
command: "entra directory-role member list",
|
|
289
|
+
key: "roleMembers",
|
|
290
|
+
noun: "role members",
|
|
291
|
+
known: MEMBER_KNOWN,
|
|
292
|
+
knownList: KNOWN_ROLE_MEMBER_FIELDS,
|
|
293
|
+
defaultSelect: DEFAULT_MEMBER_SELECT,
|
|
294
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
295
|
+
denialHints: ROLE_DENIAL_HINTS,
|
|
296
|
+
scopeNote: "Members are the principals assigned to the activated role; @odata.type names each member kind (user, group, device or service principal)",
|
|
297
|
+
emptyNote: "0 role members matched; the absence of results is the answer, not an error",
|
|
298
|
+
parent: { param: "directoryRole-id", flag: "role", label: "activated directory-role object ID whose members are listed" },
|
|
299
|
+
keepODataType: true,
|
|
300
|
+
};
|
|
301
|
+
const SCOPED_LIST = {
|
|
302
|
+
command: "entra directory-role scoped-member list",
|
|
303
|
+
key: "scopedMembers",
|
|
304
|
+
noun: "scoped members",
|
|
305
|
+
known: SCOPED_KNOWN,
|
|
306
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
307
|
+
defaultSelect: DEFAULT_SCOPED_LIST_SELECT,
|
|
308
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
309
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
310
|
+
scopeNote: "Scoped members bind one role to one principal within one directory scope; administrative-unit scopes need P1",
|
|
311
|
+
emptyNote: "0 scoped members matched; the absence of results is the answer, not an error",
|
|
312
|
+
parent: { param: "directoryRole-id", flag: "role", label: "activated directory-role object ID whose scoped members are listed" },
|
|
313
|
+
};
|
|
314
|
+
const MEMBERSHIP_LIST = {
|
|
315
|
+
command: "entra scoped-role-membership list",
|
|
316
|
+
key: "scopedRoleMemberships",
|
|
317
|
+
noun: "scoped role memberships",
|
|
318
|
+
known: SCOPED_KNOWN,
|
|
319
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
320
|
+
defaultSelect: DEFAULT_SCOPED_LIST_SELECT,
|
|
321
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
322
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
323
|
+
scopeNote: "Scoped role memberships are administrative-unit-scoped admin assignments; directory-wide assignments live on roleAssignments",
|
|
324
|
+
emptyNote: "0 scoped role memberships matched; the absence of results is the answer, not an error",
|
|
325
|
+
};
|
|
221
326
|
async function listCollection(shape, session, flags, profile, operation, help, profileName) {
|
|
222
327
|
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
223
328
|
if (cursor !== undefined && !cursor.trim())
|
|
224
329
|
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
330
|
+
let params;
|
|
331
|
+
if (shape.parent !== undefined) {
|
|
332
|
+
const parentId = String(flags[shape.parent.flag] ?? "");
|
|
333
|
+
if (!parentId.trim())
|
|
334
|
+
throw new AxiError(`--${shape.parent.flag} needs the ${shape.parent.label}`, "VALIDATION_ERROR", [help]);
|
|
335
|
+
params = { [shape.parent.param]: parentId };
|
|
336
|
+
}
|
|
225
337
|
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
226
338
|
const savedSelect = saved?.$select;
|
|
227
339
|
const { select, fields } = selectedFields(flags, savedSelect === undefined ? shape.defaultSelect : fieldList(savedSelect, "select", shape.known, shape.knownList, help), shape.known, shape.knownList, help);
|
|
@@ -235,6 +347,8 @@ async function listCollection(shape, session, flags, profile, operation, help, p
|
|
|
235
347
|
if (filter !== undefined)
|
|
236
348
|
query.$filter = filter;
|
|
237
349
|
const args = { profile, operation, query, scopes };
|
|
350
|
+
if (params !== undefined)
|
|
351
|
+
args.params = params;
|
|
238
352
|
if (cursor !== undefined)
|
|
239
353
|
args.cursor = cursor;
|
|
240
354
|
if (flags.all === true) {
|
|
@@ -251,11 +365,13 @@ async function listCollection(shape, session, flags, profile, operation, help, p
|
|
|
251
365
|
const rows = [];
|
|
252
366
|
let truncated = false;
|
|
253
367
|
for (const row of result.value) {
|
|
254
|
-
const projected = project(row, fields, full);
|
|
368
|
+
const projected = project(row, fields, full, shape.keepODataType === true);
|
|
255
369
|
rows.push(projected.row);
|
|
256
370
|
truncated = truncated || projected.truncated;
|
|
257
371
|
}
|
|
258
|
-
const showHint = shape.key === "directoryRoles" ? `mg-axi entra directory-role show --id <role-id> ${profileHint(profileName)}`
|
|
372
|
+
const showHint = shape.key === "directoryRoles" ? `mg-axi entra directory-role show --id <role-id> ${profileHint(profileName)}`
|
|
373
|
+
: shape.key === "directoryRoleTemplates" ? `mg-axi entra directory-role-template show --id <template-id> ${profileHint(profileName)}`
|
|
374
|
+
: undefined;
|
|
259
375
|
const correlateHint = shape.key === "directoryRoles"
|
|
260
376
|
? undefined
|
|
261
377
|
: `For built-in roles, roleDefinitionId matches the directory-role roleTemplateId: mg-axi entra directory-role list ${profileHint(profileName)}`;
|
|
@@ -264,16 +380,17 @@ async function listCollection(shape, session, flags, profile, operation, help, p
|
|
|
264
380
|
if (!result.complete) {
|
|
265
381
|
return {
|
|
266
382
|
[shape.key]: rows,
|
|
267
|
-
|
|
383
|
+
...listTotals(rows.length, result.total, shape.noun, false),
|
|
384
|
+
complete: false,
|
|
385
|
+
reason: result.reason,
|
|
268
386
|
cursor: result.cursor,
|
|
269
387
|
help: [...truncationHints, `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`, ...standing],
|
|
270
388
|
};
|
|
271
389
|
}
|
|
272
|
-
const count = { returned: rows.length, complete: true };
|
|
273
390
|
if (!rows.length) {
|
|
274
|
-
return { [shape.key]: rows,
|
|
391
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [shape.emptyNote, ...standing] };
|
|
275
392
|
}
|
|
276
|
-
return { [shape.key]: rows,
|
|
393
|
+
return { [shape.key]: rows, ...listTotals(rows.length, result.total, shape.noun, true), complete: true, help: [...truncationHints, ...standing] };
|
|
277
394
|
}
|
|
278
395
|
export async function listDirectoryRoles(session, flags, profile, operation, help, profileName) {
|
|
279
396
|
return listCollection(ROLE_LIST, session, flags, profile, operation, help, profileName);
|
|
@@ -308,3 +425,78 @@ export async function listPimEligible(session, flags, profile, operation, help,
|
|
|
308
425
|
export async function listPimActive(session, flags, profile, operation, help, profileName) {
|
|
309
426
|
return listCollection(PIM_ACTIVE, session, flags, profile, operation, help, profileName);
|
|
310
427
|
}
|
|
428
|
+
const TEMPLATE_SINGLE = {
|
|
429
|
+
command: "entra directory-role-template show",
|
|
430
|
+
key: "directoryRoleTemplate",
|
|
431
|
+
noun: "directory-role template",
|
|
432
|
+
known: TEMPLATE_KNOWN,
|
|
433
|
+
knownList: KNOWN_TEMPLATE_FIELDS,
|
|
434
|
+
defaultSelect: DEFAULT_TEMPLATE_SHOW_SELECT,
|
|
435
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
436
|
+
denialHints: ROLE_DENIAL_HINTS,
|
|
437
|
+
};
|
|
438
|
+
const SCOPED_SINGLE = {
|
|
439
|
+
command: "entra directory-role scoped-member show",
|
|
440
|
+
key: "scopedMember",
|
|
441
|
+
noun: "scoped member",
|
|
442
|
+
known: SCOPED_KNOWN,
|
|
443
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
444
|
+
defaultSelect: DEFAULT_SCOPED_SHOW_SELECT,
|
|
445
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
446
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
447
|
+
};
|
|
448
|
+
const MEMBERSHIP_SINGLE = {
|
|
449
|
+
command: "entra scoped-role-membership show",
|
|
450
|
+
key: "scopedRoleMembership",
|
|
451
|
+
noun: "scoped role membership",
|
|
452
|
+
known: SCOPED_KNOWN,
|
|
453
|
+
knownList: KNOWN_SCOPED_MEMBER_FIELDS,
|
|
454
|
+
defaultSelect: DEFAULT_SCOPED_SHOW_SELECT,
|
|
455
|
+
defaultScopes: DEFAULT_ROLE_SCOPES,
|
|
456
|
+
denialHints: SCOPED_DENIAL_HINTS,
|
|
457
|
+
};
|
|
458
|
+
async function showSingle(shape, params, session, flags, profile, operation, help, profileName) {
|
|
459
|
+
const { select, fields } = selectedFields(flags, shape.defaultSelect, shape.known, shape.knownList, help);
|
|
460
|
+
const scopes = scopesFor(flags, profile, shape.defaultScopes, help);
|
|
461
|
+
const full = flags.full === true;
|
|
462
|
+
const raw = await withGuidance(shape.denialHints, () => session.execute({
|
|
463
|
+
profile,
|
|
464
|
+
operation,
|
|
465
|
+
params,
|
|
466
|
+
query: { $select: select.join(",") },
|
|
467
|
+
scopes,
|
|
468
|
+
}));
|
|
469
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
470
|
+
throw new AxiError(`Graph returned a malformed ${shape.noun} body`, "GRAPH_ERROR", [
|
|
471
|
+
`Single-${shape.noun} reads carry one object; treat anything else as unknown, not empty`,
|
|
472
|
+
]);
|
|
473
|
+
}
|
|
474
|
+
const { row, truncated } = project(raw, fields, full);
|
|
475
|
+
if (truncated)
|
|
476
|
+
return { [shape.key]: row, help: [fullHint(shape.command, flags, profileName)] };
|
|
477
|
+
return { [shape.key]: row };
|
|
478
|
+
}
|
|
479
|
+
export async function listDirectoryRoleTemplates(session, flags, profile, operation, help, profileName) {
|
|
480
|
+
return listCollection(TEMPLATE_LIST, session, flags, profile, operation, help, profileName);
|
|
481
|
+
}
|
|
482
|
+
export async function showDirectoryRoleTemplate(session, flags, profile, operation, help, profileName) {
|
|
483
|
+
return showSingle(TEMPLATE_SINGLE, { "directoryRoleTemplate-id": String(flags.id) }, session, flags, profile, operation, help, profileName);
|
|
484
|
+
}
|
|
485
|
+
export async function listRoleMembers(session, flags, profile, operation, help, profileName) {
|
|
486
|
+
return listCollection(MEMBER_LIST, session, flags, profile, operation, help, profileName);
|
|
487
|
+
}
|
|
488
|
+
export async function listScopedMembers(session, flags, profile, operation, help, profileName) {
|
|
489
|
+
return listCollection(SCOPED_LIST, session, flags, profile, operation, help, profileName);
|
|
490
|
+
}
|
|
491
|
+
export async function showScopedMember(session, flags, profile, operation, help, profileName) {
|
|
492
|
+
const roleId = String(flags.role ?? "");
|
|
493
|
+
if (!roleId.trim())
|
|
494
|
+
throw new AxiError("--role needs the activated directory-role object ID owning the scoped member", "VALIDATION_ERROR", [help]);
|
|
495
|
+
return showSingle(SCOPED_SINGLE, { "directoryRole-id": roleId, "scopedRoleMembership-id": String(flags.id) }, session, flags, profile, operation, help, profileName);
|
|
496
|
+
}
|
|
497
|
+
export async function listScopedRoleMemberships(session, flags, profile, operation, help, profileName) {
|
|
498
|
+
return listCollection(MEMBERSHIP_LIST, session, flags, profile, operation, help, profileName);
|
|
499
|
+
}
|
|
500
|
+
export async function showScopedRoleMembership(session, flags, profile, operation, help, profileName) {
|
|
501
|
+
return showSingle(MEMBERSHIP_SINGLE, { "scopedRoleMembership-id": String(flags.id) }, session, flags, profile, operation, help, profileName);
|
|
502
|
+
}
|