@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
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
// EXT-03 risk-prevention subfamily: the read mapping behind
|
|
3
|
+
// `mg-axi entra fraud-protection-provider list/show/count`,
|
|
4
|
+
// `mg-axi entra web-application-firewall-provider list/show/count` and
|
|
5
|
+
// `mg-axi entra web-application-firewall-verification list/show/count`
|
|
6
|
+
// plus `... verification provider show`. Operation construction stays
|
|
7
|
+
// beside its command; the shared session owns URLs, credentials, paging,
|
|
8
|
+
// retries and error translation, and the SDK owns TOON rendering. This
|
|
9
|
+
// module only maps flags to session calls and projects rows for compact
|
|
10
|
+
// output.
|
|
11
|
+
//
|
|
12
|
+
// Reviewed against the v1.0 riskpreventioncontainer-list-fraudprotectionproviders,
|
|
13
|
+
// fraudprotectionprovider-get,
|
|
14
|
+
// riskpreventioncontainer-list-webapplicationfirewallproviders,
|
|
15
|
+
// webapplicationfirewallprovider-get,
|
|
16
|
+
// riskpreventioncontainer-list-webapplicationfirewallverifications and
|
|
17
|
+
// webapplicationfirewallverificationmodel-get operation documentation and
|
|
18
|
+
// the fraudProtectionProvider, arkoseFraudProtectionProvider,
|
|
19
|
+
// humanSecurityFraudProtectionProvider, webApplicationFirewallProvider,
|
|
20
|
+
// akamaiWebApplicationFirewallProvider,
|
|
21
|
+
// cloudFlareWebApplicationFirewallProvider and
|
|
22
|
+
// webApplicationFirewallVerificationModel resource references on
|
|
23
|
+
// 2026-10-06. All six operation pages document the same access contract,
|
|
24
|
+
// so one scope and one denial-hint set serve all ten reads. Every read
|
|
25
|
+
// takes delegated RiskPreventionProviders.Read.All
|
|
26
|
+
// (RiskPreventionProviders.ReadWrite.All is the documented
|
|
27
|
+
// higher-privileged alternative); Graph documents no supported
|
|
28
|
+
// application permission, so application profiles are refused before
|
|
29
|
+
// credentials rather than sent to a certain denial. Delegated callers
|
|
30
|
+
// additionally need a directory role that can read risk-prevention
|
|
31
|
+
// configuration (Security Reader is the least-privileged read-only role;
|
|
32
|
+
// External ID User Flow Administrator, Application Administrator,
|
|
33
|
+
// Directory Reader and Security Administrator also work). Personal
|
|
34
|
+
// Microsoft accounts are not supported. No per-operation licence
|
|
35
|
+
// prerequisite is stated for these reads. The fraud list documents
|
|
36
|
+
// $select only, so it offers no --filter; the WAF and verification
|
|
37
|
+
// lists document $filter and offer it as plain $filter with no $count
|
|
38
|
+
// or ConsistencyLevel contract. $expand stays out everywhere: the
|
|
39
|
+
// verification's provider has its own sub-read and file/detail
|
|
40
|
+
// navigations need their own review. The three $count scalars take no
|
|
41
|
+
// --filter/--select/--limit/--cursor. The provider sub-read has no
|
|
42
|
+
// dedicated operation page; access follows the WAF provider-get
|
|
43
|
+
// contract and the verificationModel resource reference. Beta stays out
|
|
44
|
+
// ("beta needs its own review") and no mutation lives here: provider
|
|
45
|
+
// create/update/delete and verification submission (POST
|
|
46
|
+
// .../webApplicationFirewallProviders/{id}/verify) belong to no read
|
|
47
|
+
// slice.
|
|
48
|
+
//
|
|
49
|
+
// Secrecy by construction: API keys, client secrets and other key
|
|
50
|
+
// material the provider shapes carry (Arkose privateKey and publicKey,
|
|
51
|
+
// HUMAN serverToken, Akamai clientSecret/clientToken/accessToken,
|
|
52
|
+
// Cloudflare apiToken) are not selectable, so --select/--fields naming
|
|
53
|
+
// them fails before credentials, and rows the server returns carrying
|
|
54
|
+
// any of them are scrubbed before projection. Secrets therefore can
|
|
55
|
+
// never reach stdout, errors, previews or logs through this slice.
|
|
56
|
+
// publicKey is key material even though it is public: it follows the
|
|
57
|
+
// identity-provider certificateData precedent and is never returned.
|
|
58
|
+
// @odata.type is preserved on fraud and WAF rows without being
|
|
59
|
+
// selectable: it names the provider kind (for example
|
|
60
|
+
// #microsoft.graph.arkoseFraudProtectionProvider).
|
|
61
|
+
// Every fraud-protection-provider property this slice may request or
|
|
62
|
+
// display: the base properties plus the non-secret derived properties
|
|
63
|
+
// (Arkose clientSubDomain/verifySubDomain, HUMAN appId). Anything else
|
|
64
|
+
// fails before credentials.
|
|
65
|
+
export const KNOWN_FRAUD_FIELDS = [
|
|
66
|
+
"id",
|
|
67
|
+
"displayName",
|
|
68
|
+
"clientSubDomain",
|
|
69
|
+
"verifySubDomain",
|
|
70
|
+
"appId",
|
|
71
|
+
];
|
|
72
|
+
const KNOWN_FRAUD = new Set(KNOWN_FRAUD_FIELDS);
|
|
73
|
+
// Every WAF-provider property this slice may request or display: the
|
|
74
|
+
// base properties plus the non-secret derived properties (Akamai
|
|
75
|
+
// hostPrefix, Cloudflare zoneId). Anything else fails before
|
|
76
|
+
// credentials.
|
|
77
|
+
export const KNOWN_WAF_FIELDS = [
|
|
78
|
+
"id",
|
|
79
|
+
"displayName",
|
|
80
|
+
"hostPrefix",
|
|
81
|
+
"zoneId",
|
|
82
|
+
];
|
|
83
|
+
const KNOWN_WAF = new Set(KNOWN_WAF_FIELDS);
|
|
84
|
+
// Every verification property this slice may request or display,
|
|
85
|
+
// matching the reviewed resource. verificationResult and
|
|
86
|
+
// verifiedDetails ride whole as complex objects. Anything else fails
|
|
87
|
+
// before credentials.
|
|
88
|
+
export const KNOWN_VERIFICATION_FIELDS = [
|
|
89
|
+
"id",
|
|
90
|
+
"verifiedHost",
|
|
91
|
+
"providerType",
|
|
92
|
+
"verificationResult",
|
|
93
|
+
"verifiedDetails",
|
|
94
|
+
];
|
|
95
|
+
const KNOWN_VERIFICATIONS = new Set(KNOWN_VERIFICATION_FIELDS);
|
|
96
|
+
// Secret-bearing keys are never selectable and never projected. The
|
|
97
|
+
// server is not expected to return them on GET, but any row carrying
|
|
98
|
+
// them is scrubbed before projection so key material can never reach
|
|
99
|
+
// output.
|
|
100
|
+
const SECRET_RISK_FIELDS = [
|
|
101
|
+
"privateKey",
|
|
102
|
+
"publicKey",
|
|
103
|
+
"serverToken",
|
|
104
|
+
"clientSecret",
|
|
105
|
+
"clientToken",
|
|
106
|
+
"accessToken",
|
|
107
|
+
"apiToken",
|
|
108
|
+
];
|
|
109
|
+
// @odata.type is preserved on fraud and WAF rows without being
|
|
110
|
+
// selectable: it names the derived provider kind.
|
|
111
|
+
const RISK_TYPE_PROPERTY = "@odata.type";
|
|
112
|
+
// Compact rows: identifier and name; the kind rides as @odata.type.
|
|
113
|
+
const DEFAULT_FRAUD_LIST_SELECT = ["id", "displayName"];
|
|
114
|
+
// Show rows: the full reviewed set.
|
|
115
|
+
const DEFAULT_FRAUD_SHOW_SELECT = [...KNOWN_FRAUD_FIELDS];
|
|
116
|
+
// Compact rows: identifier and name; the kind rides as @odata.type.
|
|
117
|
+
const DEFAULT_WAF_LIST_SELECT = ["id", "displayName"];
|
|
118
|
+
// Show rows: the full reviewed set.
|
|
119
|
+
const DEFAULT_WAF_SHOW_SELECT = [...KNOWN_WAF_FIELDS];
|
|
120
|
+
// Compact rows: identifier, verified host and provider type.
|
|
121
|
+
const DEFAULT_VERIFICATION_LIST_SELECT = ["id", "verifiedHost", "providerType"];
|
|
122
|
+
// Show rows: the full reviewed set.
|
|
123
|
+
const DEFAULT_VERIFICATION_SHOW_SELECT = [...KNOWN_VERIFICATION_FIELDS];
|
|
124
|
+
// Delegated-only reads: Graph documents no supported application
|
|
125
|
+
// permission, so application profiles are refused before credentials.
|
|
126
|
+
export const DEFAULT_RISK_SCOPES = ["https://graph.microsoft.com/RiskPreventionProviders.Read.All"];
|
|
127
|
+
const TRUNCATE_AT = 500;
|
|
128
|
+
const FRAUD_KIND = {
|
|
129
|
+
noun: "fraud-protection-provider",
|
|
130
|
+
known: KNOWN_FRAUD,
|
|
131
|
+
knownList: KNOWN_FRAUD_FIELDS,
|
|
132
|
+
listSelect: DEFAULT_FRAUD_LIST_SELECT,
|
|
133
|
+
showSelect: DEFAULT_FRAUD_SHOW_SELECT,
|
|
134
|
+
collectionKey: "fraudProtectionProviders",
|
|
135
|
+
singleKey: "fraudProtectionProvider",
|
|
136
|
+
keepType: true,
|
|
137
|
+
};
|
|
138
|
+
const WAF_KIND = {
|
|
139
|
+
noun: "web-application-firewall-provider",
|
|
140
|
+
known: KNOWN_WAF,
|
|
141
|
+
knownList: KNOWN_WAF_FIELDS,
|
|
142
|
+
listSelect: DEFAULT_WAF_LIST_SELECT,
|
|
143
|
+
showSelect: DEFAULT_WAF_SHOW_SELECT,
|
|
144
|
+
collectionKey: "webApplicationFirewallProviders",
|
|
145
|
+
singleKey: "webApplicationFirewallProvider",
|
|
146
|
+
keepType: true,
|
|
147
|
+
};
|
|
148
|
+
const VERIFICATION_KIND = {
|
|
149
|
+
noun: "web-application-firewall-verification",
|
|
150
|
+
known: KNOWN_VERIFICATIONS,
|
|
151
|
+
knownList: KNOWN_VERIFICATION_FIELDS,
|
|
152
|
+
listSelect: DEFAULT_VERIFICATION_LIST_SELECT,
|
|
153
|
+
showSelect: DEFAULT_VERIFICATION_SHOW_SELECT,
|
|
154
|
+
collectionKey: "webApplicationFirewallVerifications",
|
|
155
|
+
singleKey: "webApplicationFirewallVerification",
|
|
156
|
+
keepType: false,
|
|
157
|
+
};
|
|
158
|
+
function fieldList(raw, kind, flag, help) {
|
|
159
|
+
const parts = String(raw)
|
|
160
|
+
.split(",")
|
|
161
|
+
.map(part => part.trim())
|
|
162
|
+
.filter(part => part.length > 0);
|
|
163
|
+
if (!parts.length)
|
|
164
|
+
throw new AxiError(`--${flag} needs at least one property`, "VALIDATION_ERROR", [help]);
|
|
165
|
+
const fields = [];
|
|
166
|
+
for (const part of parts) {
|
|
167
|
+
if (!kind.known.has(part)) {
|
|
168
|
+
const secret = SECRET_RISK_FIELDS.includes(part);
|
|
169
|
+
throw new AxiError(secret
|
|
170
|
+
? `--${flag} cannot request ${part}: secret-bearing provider fields are never returned`
|
|
171
|
+
: `Unknown ${kind.noun} property ${part} in --${flag}`, "VALIDATION_ERROR", [
|
|
172
|
+
help,
|
|
173
|
+
`Known ${kind.noun} properties: ${kind.knownList.join(", ")}`,
|
|
174
|
+
]);
|
|
175
|
+
}
|
|
176
|
+
if (!fields.includes(part))
|
|
177
|
+
fields.push(part);
|
|
178
|
+
}
|
|
179
|
+
return fields;
|
|
180
|
+
}
|
|
181
|
+
function scopesFor(flags, profile, help, noun) {
|
|
182
|
+
if (profile.mode === "application") {
|
|
183
|
+
throw new AxiError(`${noun} reads need a delegated profile; Graph documents no supported application permission for this operation`, "VALIDATION_ERROR", [help, "mg-axi profile create --name <name> --tenant <tenant-id> --client <client-id> --cloud commercial"]);
|
|
184
|
+
}
|
|
185
|
+
if (flags.scopes === undefined)
|
|
186
|
+
return [...DEFAULT_RISK_SCOPES];
|
|
187
|
+
const scopes = String(flags.scopes)
|
|
188
|
+
.split(",")
|
|
189
|
+
.map(scope => scope.trim())
|
|
190
|
+
.filter(scope => scope.length > 0);
|
|
191
|
+
return scopes;
|
|
192
|
+
}
|
|
193
|
+
function truncateValue(value, full) {
|
|
194
|
+
const truncate = (text) => full || text.length <= TRUNCATE_AT
|
|
195
|
+
? { value: text, truncated: false }
|
|
196
|
+
: { value: `${text.slice(0, TRUNCATE_AT)}... (truncated, ${text.length} chars total)`, truncated: true };
|
|
197
|
+
if (typeof value === "string")
|
|
198
|
+
return truncate(value);
|
|
199
|
+
if (Array.isArray(value)) {
|
|
200
|
+
const projected = value.map(entry => truncateValue(entry, full));
|
|
201
|
+
return { value: projected.map(entry => entry.value), truncated: projected.some(entry => entry.truncated) };
|
|
202
|
+
}
|
|
203
|
+
if (value !== null && typeof value === "object") {
|
|
204
|
+
const projected = Object.entries(value).map(([key, entry]) => [key, truncateValue(entry, full)]);
|
|
205
|
+
return {
|
|
206
|
+
value: Object.fromEntries(projected.map(([key, entry]) => [key, entry.value])),
|
|
207
|
+
truncated: projected.some(([, entry]) => entry.truncated),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return { value, truncated: false };
|
|
211
|
+
}
|
|
212
|
+
// Scrubbing runs before projection: any secret-bearing key the server
|
|
213
|
+
// returned is dropped so it can never be selected, rendered or leaked.
|
|
214
|
+
function scrub(row) {
|
|
215
|
+
const source = row !== null && typeof row === "object" && !Array.isArray(row) ? row : {};
|
|
216
|
+
const cleaned = {};
|
|
217
|
+
for (const [key, value] of Object.entries(source)) {
|
|
218
|
+
if (SECRET_RISK_FIELDS.includes(key))
|
|
219
|
+
continue;
|
|
220
|
+
cleaned[key] = value;
|
|
221
|
+
}
|
|
222
|
+
return cleaned;
|
|
223
|
+
}
|
|
224
|
+
// Local projection preserves Graph's null/missing distinction: an
|
|
225
|
+
// explicit null stays null, an absent property stays absent and is never
|
|
226
|
+
// synthesized. @odata.type survives projection on provider rows so the
|
|
227
|
+
// provider kind is never lost.
|
|
228
|
+
function project(row, kind, fields, full) {
|
|
229
|
+
const source = scrub(row);
|
|
230
|
+
const projected = {};
|
|
231
|
+
let truncated = false;
|
|
232
|
+
for (const field of fields) {
|
|
233
|
+
if (!Object.hasOwn(source, field))
|
|
234
|
+
continue;
|
|
235
|
+
const result = truncateValue(source[field], full);
|
|
236
|
+
projected[field] = result.value;
|
|
237
|
+
truncated = truncated || result.truncated;
|
|
238
|
+
}
|
|
239
|
+
if (kind.keepType && typeof source[RISK_TYPE_PROPERTY] === "string")
|
|
240
|
+
projected[RISK_TYPE_PROPERTY] = source[RISK_TYPE_PROPERTY];
|
|
241
|
+
return { row: projected, truncated };
|
|
242
|
+
}
|
|
243
|
+
function selectedFields(flags, kind, defaults, help) {
|
|
244
|
+
const select = flags.select === undefined ? [...defaults] : fieldList(flags.select, kind, "select", help);
|
|
245
|
+
const fields = flags.fields === undefined ? [...select] : fieldList(flags.fields, kind, "fields", help);
|
|
246
|
+
const missing = fields.find(field => !select.includes(field));
|
|
247
|
+
if (missing) {
|
|
248
|
+
throw new AxiError(`--fields ${missing} was not fetched; request it with --select`, "VALIDATION_ERROR", [help]);
|
|
249
|
+
}
|
|
250
|
+
return { select, fields };
|
|
251
|
+
}
|
|
252
|
+
function profileHint(profileName) {
|
|
253
|
+
return `--profile ${shellValue(profileName)}`;
|
|
254
|
+
}
|
|
255
|
+
function shellValue(value) {
|
|
256
|
+
return /^[A-Za-z0-9_.,:/@=-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
257
|
+
}
|
|
258
|
+
function fullHint(command, flags, profileName) {
|
|
259
|
+
const args = Object.entries({ ...flags, profile: profileName, full: true })
|
|
260
|
+
.map(([name, value]) => value === true ? `--${name}` : `--${name} ${shellValue(String(value))}`);
|
|
261
|
+
return `mg-axi ${command} ${args.join(" ")}`;
|
|
262
|
+
}
|
|
263
|
+
// Denials carry the session's generic grant/role/licence cause; these
|
|
264
|
+
// reads add the scope, roles and licensing that actually unlock them,
|
|
265
|
+
// because a 403 alone never says which prerequisite is missing.
|
|
266
|
+
function withGuidance(hints, run) {
|
|
267
|
+
return run().catch(error => {
|
|
268
|
+
if (error instanceof AxiError && error.code === "GRAPH_ERROR" && /\(403\)/.test(error.message)) {
|
|
269
|
+
throw new AxiError(error.message, "GRAPH_ERROR", [...hints, ...error.suggestions]);
|
|
270
|
+
}
|
|
271
|
+
throw error;
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
const RISK_DENIAL_HINTS = [
|
|
275
|
+
"Risk-prevention reads need delegated RiskPreventionProviders.Read.All (RiskPreventionProviders.ReadWrite.All is the documented higher-privileged alternative); delegated callers pass it as --scopes",
|
|
276
|
+
"Delegated reads additionally need a directory role that can read risk-prevention configuration (Security Reader is the least-privileged read-only role; External ID User Flow Administrator, Application Administrator, Directory Reader and Security Administrator also work); Graph documents no supported application permission, so application profiles are refused before credentials",
|
|
277
|
+
"Personal Microsoft accounts are not supported for risk-prevention reads",
|
|
278
|
+
"No per-operation licence prerequisite is stated for risk-prevention reads; never diagnose licence solely from HTTP 403",
|
|
279
|
+
];
|
|
280
|
+
const WORKFORCE_NOTE = "Workforce tenant context only; verification submission and provider create/update/delete belong to no read slice and external-customer user flows, authentication event flows and provisioning stay scheduled";
|
|
281
|
+
// Restoring the saved select/filter keeps cursor resumes lossless when
|
|
282
|
+
// --select or --filter is omitted. The fraud list documents $select
|
|
283
|
+
// only, so its leaf declares no --filter flag and strict input
|
|
284
|
+
// validation refuses the flag before credentials; the WAF and
|
|
285
|
+
// verification lists document $filter, so --filter passes through as
|
|
286
|
+
// plain $filter with no $count or ConsistencyLevel contract.
|
|
287
|
+
function collectionCommon(session, flags, kind, operation, help, profile) {
|
|
288
|
+
const cursor = flags.cursor === undefined ? undefined : String(flags.cursor);
|
|
289
|
+
if (cursor !== undefined && !cursor.trim())
|
|
290
|
+
throw new AxiError("--cursor needs the opaque cursor from a partial result", "VALIDATION_ERROR", [help]);
|
|
291
|
+
const saved = cursor === undefined ? undefined : session.cursorQuery(operation, cursor);
|
|
292
|
+
const { select, fields } = selectedFields(flags, kind, saved?.["$select"] === undefined ? kind.listSelect : fieldList(saved["$select"], kind, "select", help), help);
|
|
293
|
+
const filter = flags.filter === undefined ? saved?.["$filter"] : String(flags.filter);
|
|
294
|
+
return { cursor, select, fields, scopes: scopesFor(flags, profile, help, "Risk-prevention"), full: flags.full === true, filter };
|
|
295
|
+
}
|
|
296
|
+
function collectArgs(profile, operation, common, flags, help) {
|
|
297
|
+
const query = { $select: common.select.join(",") };
|
|
298
|
+
if (common.filter !== undefined)
|
|
299
|
+
query.$filter = common.filter;
|
|
300
|
+
const args = { profile, operation, query, scopes: common.scopes };
|
|
301
|
+
if (common.cursor !== undefined)
|
|
302
|
+
args.cursor = common.cursor;
|
|
303
|
+
if (flags.all === true) {
|
|
304
|
+
if (flags.limit !== undefined)
|
|
305
|
+
throw new AxiError("--limit and --all cannot be combined", "VALIDATION_ERROR", [help]);
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
args.limit = flags.limit === undefined ? 100 : Number(flags.limit);
|
|
309
|
+
}
|
|
310
|
+
return args;
|
|
311
|
+
}
|
|
312
|
+
function resumeHint(profileName) {
|
|
313
|
+
return `Resume losslessly with the same flags plus --cursor <cursor-from-output> ${profileHint(profileName)}`;
|
|
314
|
+
}
|
|
315
|
+
function resourceId(flags, help, noun) {
|
|
316
|
+
const id = String(flags.id);
|
|
317
|
+
if (!id.trim())
|
|
318
|
+
throw new AxiError(`--id needs the ${noun} ID`, "VALIDATION_ERROR", [help]);
|
|
319
|
+
return id;
|
|
320
|
+
}
|
|
321
|
+
function singleResult(raw, kind, fields, full) {
|
|
322
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
323
|
+
throw new AxiError(`Graph returned a malformed ${kind.noun} body`, "GRAPH_ERROR", [
|
|
324
|
+
`Single-${kind.noun} reads carry one ${kind.singleKey} object; treat anything else as unknown, not empty`,
|
|
325
|
+
]);
|
|
326
|
+
}
|
|
327
|
+
return project(raw, kind, fields, full);
|
|
328
|
+
}
|
|
329
|
+
async function collectKind(session, flags, kind, command, showCommand, profile, operation, help, profileName) {
|
|
330
|
+
const common = collectionCommon(session, flags, kind, operation, help, profile);
|
|
331
|
+
const result = await withGuidance(RISK_DENIAL_HINTS, () => session.collect(collectArgs(profile, operation, common, flags, help)));
|
|
332
|
+
const effectiveFlags = { ...flags, select: result.query.$select ?? kind.listSelect.join(",") };
|
|
333
|
+
if (result.query.$filter !== undefined)
|
|
334
|
+
effectiveFlags.filter = result.query.$filter;
|
|
335
|
+
const rows = [];
|
|
336
|
+
let truncated = false;
|
|
337
|
+
for (const row of result.value) {
|
|
338
|
+
const projected = project(row, kind, common.fields, common.full);
|
|
339
|
+
rows.push(projected.row);
|
|
340
|
+
truncated = truncated || projected.truncated;
|
|
341
|
+
}
|
|
342
|
+
const showHint = `mg-axi ${showCommand} --id <${kind.noun}-id> ${profileHint(profileName)}`;
|
|
343
|
+
const truncationHints = truncated ? [fullHint(command, effectiveFlags, profileName)] : [];
|
|
344
|
+
if (!result.complete) {
|
|
345
|
+
return {
|
|
346
|
+
[kind.collectionKey]: rows,
|
|
347
|
+
count: { returned: rows.length, complete: false, reason: result.reason },
|
|
348
|
+
cursor: result.cursor,
|
|
349
|
+
help: [...truncationHints, resumeHint(profileName), showHint, WORKFORCE_NOTE],
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
const count = { returned: rows.length, complete: true };
|
|
353
|
+
if (!rows.length) {
|
|
354
|
+
return {
|
|
355
|
+
[kind.collectionKey]: rows,
|
|
356
|
+
count,
|
|
357
|
+
help: [`0 ${kind.noun}s matched; the absence of results is the answer, not an error`, WORKFORCE_NOTE],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return { [kind.collectionKey]: rows, count, help: [...truncationHints, showHint, WORKFORCE_NOTE] };
|
|
361
|
+
}
|
|
362
|
+
async function showKind(session, flags, kind, command, param, profile, operation, help, profileName) {
|
|
363
|
+
const { select, fields } = selectedFields(flags, kind, kind.showSelect, help);
|
|
364
|
+
const scopes = scopesFor(flags, profile, help, "Risk-prevention");
|
|
365
|
+
const full = flags.full === true;
|
|
366
|
+
const id = resourceId(flags, help, kind.noun);
|
|
367
|
+
const raw = await withGuidance(RISK_DENIAL_HINTS, () => session.execute({
|
|
368
|
+
profile,
|
|
369
|
+
operation,
|
|
370
|
+
params: { [param]: id },
|
|
371
|
+
query: { $select: select.join(",") },
|
|
372
|
+
scopes,
|
|
373
|
+
}));
|
|
374
|
+
const { row, truncated } = singleResult(raw, kind, fields, full);
|
|
375
|
+
if (truncated)
|
|
376
|
+
return { [kind.singleKey]: row, help: [fullHint(command, flags, profileName), WORKFORCE_NOTE] };
|
|
377
|
+
return { [kind.singleKey]: row, help: [WORKFORCE_NOTE] };
|
|
378
|
+
}
|
|
379
|
+
async function countKind(session, flags, kind, profile, operation, help) {
|
|
380
|
+
const scopes = scopesFor(flags, profile, help, "Risk-prevention");
|
|
381
|
+
const raw = await withGuidance(RISK_DENIAL_HINTS, () => session.execute({ profile, operation, scopes, scalar: true }));
|
|
382
|
+
if (typeof raw !== "number" || !Number.isInteger(raw) || raw < 0) {
|
|
383
|
+
throw new AxiError(`Graph returned a malformed ${kind.noun} count body`, "GRAPH_ERROR", [
|
|
384
|
+
`${kind.noun} counts carry one non-negative integer scalar; treat anything else as unknown, not empty`,
|
|
385
|
+
]);
|
|
386
|
+
}
|
|
387
|
+
return { count: { returned: raw, complete: true }, help: [WORKFORCE_NOTE] };
|
|
388
|
+
}
|
|
389
|
+
export async function listFraudProtectionProviders(session, flags, profile, operation, help, profileName) {
|
|
390
|
+
return collectKind(session, flags, FRAUD_KIND, "entra fraud-protection-provider list", "entra fraud-protection-provider show", profile, operation, help, profileName);
|
|
391
|
+
}
|
|
392
|
+
export async function showFraudProtectionProvider(session, flags, profile, operation, help, profileName) {
|
|
393
|
+
return showKind(session, flags, FRAUD_KIND, "entra fraud-protection-provider show", "fraudProtectionProvider-id", profile, operation, help, profileName);
|
|
394
|
+
}
|
|
395
|
+
export async function countFraudProtectionProviders(session, flags, profile, operation, help) {
|
|
396
|
+
return countKind(session, flags, FRAUD_KIND, profile, operation, help);
|
|
397
|
+
}
|
|
398
|
+
export async function listWafProviders(session, flags, profile, operation, help, profileName) {
|
|
399
|
+
return collectKind(session, flags, WAF_KIND, "entra web-application-firewall-provider list", "entra web-application-firewall-provider show", profile, operation, help, profileName);
|
|
400
|
+
}
|
|
401
|
+
export async function showWafProvider(session, flags, profile, operation, help, profileName) {
|
|
402
|
+
return showKind(session, flags, WAF_KIND, "entra web-application-firewall-provider show", "webApplicationFirewallProvider-id", profile, operation, help, profileName);
|
|
403
|
+
}
|
|
404
|
+
export async function countWafProviders(session, flags, profile, operation, help) {
|
|
405
|
+
return countKind(session, flags, WAF_KIND, profile, operation, help);
|
|
406
|
+
}
|
|
407
|
+
export async function listWafVerifications(session, flags, profile, operation, help, profileName) {
|
|
408
|
+
return collectKind(session, flags, VERIFICATION_KIND, "entra web-application-firewall-verification list", "entra web-application-firewall-verification show", profile, operation, help, profileName);
|
|
409
|
+
}
|
|
410
|
+
export async function showWafVerification(session, flags, profile, operation, help, profileName) {
|
|
411
|
+
return showKind(session, flags, VERIFICATION_KIND, "entra web-application-firewall-verification show", "webApplicationFirewallVerificationModel-id", profile, operation, help, profileName);
|
|
412
|
+
}
|
|
413
|
+
export async function countWafVerifications(session, flags, profile, operation, help) {
|
|
414
|
+
return countKind(session, flags, VERIFICATION_KIND, profile, operation, help);
|
|
415
|
+
}
|
|
416
|
+
export async function showVerificationProvider(session, flags, profile, operation, help, profileName) {
|
|
417
|
+
const { select, fields } = selectedFields(flags, WAF_KIND, WAF_KIND.showSelect, help);
|
|
418
|
+
const scopes = scopesFor(flags, profile, help, "Risk-prevention");
|
|
419
|
+
const full = flags.full === true;
|
|
420
|
+
const id = resourceId(flags, help, VERIFICATION_KIND.noun);
|
|
421
|
+
const raw = await withGuidance(RISK_DENIAL_HINTS, () => session.execute({
|
|
422
|
+
profile,
|
|
423
|
+
operation,
|
|
424
|
+
params: { "webApplicationFirewallVerificationModel-id": id },
|
|
425
|
+
query: { $select: select.join(",") },
|
|
426
|
+
scopes,
|
|
427
|
+
}));
|
|
428
|
+
const { row, truncated } = singleResult(raw, WAF_KIND, fields, full);
|
|
429
|
+
if (truncated) {
|
|
430
|
+
return {
|
|
431
|
+
[WAF_KIND.singleKey]: row,
|
|
432
|
+
help: [fullHint("entra web-application-firewall-verification provider show", flags, profileName), WORKFORCE_NOTE],
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
return { [WAF_KIND.singleKey]: row, help: [WORKFORCE_NOTE] };
|
|
436
|
+
}
|