@knowttl/mg-axi 0.1.2 → 0.1.4
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 +127 -45
- package/dist/api.js +119 -0
- package/dist/app-auth.js +5 -1
- package/dist/catalogue.js +311 -3
- package/dist/cli.js +44 -12
- package/dist/docs.js +139 -47
- package/dist/entra-apps.js +158 -6
- package/dist/entra-audit-logs.js +93 -12
- package/dist/entra-conditional-access.js +416 -11
- 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-identity-providers.js +7 -4
- package/dist/entra-lifecycle-workflows.js +16 -4
- package/dist/entra-multi-tenant-organization.js +7 -4
- package/dist/entra-on-premises-synchronization.js +6 -4
- package/dist/entra-risk-prevention.js +10 -4
- package/dist/entra-risk.js +11 -4
- package/dist/entra-roles.js +194 -8
- package/dist/entra-terms-of-use.js +13 -8
- package/dist/graph-session.js +1 -0
- package/dist/msal-app-provider.js +96 -13
- package/dist/profiles.js +55 -9
- package/dist/setup.js +5 -2
- package/docs/coverage.md +721 -654
- package/inventory/operations.json +2 -2
- package/inventory/schema.json +1 -1
- package/package.json +1 -1
- package/skills/mg-axi/SKILL.md +34 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AxiError } from "axi-sdk-js";
|
|
2
|
+
import { listTotals } from "./list-totals.js";
|
|
2
3
|
// EXT-01 terms-of-use subfamily: the read mapping behind
|
|
3
4
|
// `mg-axi entra agreement list/show`,
|
|
4
5
|
// `mg-axi entra agreement acceptance list/show --agreement <agreement-id>`
|
|
@@ -264,20 +265,22 @@ export async function listAgreements(session, flags, profile, operation, help, p
|
|
|
264
265
|
if (!result.complete) {
|
|
265
266
|
return {
|
|
266
267
|
agreements,
|
|
267
|
-
|
|
268
|
+
...listTotals(agreements.length, result.total, "agreements", false),
|
|
269
|
+
complete: false,
|
|
270
|
+
reason: result.reason,
|
|
268
271
|
cursor: result.cursor,
|
|
269
272
|
help: [...truncationHints, resumeHint(profileName), showHint],
|
|
270
273
|
};
|
|
271
274
|
}
|
|
272
|
-
const count = { returned: agreements.length, complete: true };
|
|
273
275
|
if (!agreements.length) {
|
|
274
276
|
return {
|
|
275
277
|
agreements,
|
|
276
|
-
|
|
278
|
+
...listTotals(agreements.length, result.total, "agreements", true),
|
|
279
|
+
complete: true,
|
|
277
280
|
help: ["0 agreements matched; no terms-of-use agreements are configured for the tenant, or no row passed --filter"],
|
|
278
281
|
};
|
|
279
282
|
}
|
|
280
|
-
return { agreements,
|
|
283
|
+
return { agreements, ...listTotals(agreements.length, result.total, "agreements", true), complete: true, help: [...truncationHints, showHint] };
|
|
281
284
|
}
|
|
282
285
|
export async function showAgreement(session, flags, profile, operation, help, profileName) {
|
|
283
286
|
const { select, fields } = selectedFields(flags, KNOWN_AGREEMENTS, KNOWN_AGREEMENT_FIELDS, DEFAULT_AGREEMENT_SHOW_SELECT, help, "agreement");
|
|
@@ -325,20 +328,22 @@ async function collectAcceptances(session, flags, profile, operation, help, prof
|
|
|
325
328
|
if (!result.complete) {
|
|
326
329
|
return {
|
|
327
330
|
agreementAcceptances: acceptances,
|
|
328
|
-
|
|
331
|
+
...listTotals(acceptances.length, result.total, "agreement acceptances", false),
|
|
332
|
+
complete: false,
|
|
333
|
+
reason: result.reason,
|
|
329
334
|
cursor: result.cursor,
|
|
330
335
|
help: [...truncationHints, resumeHint(profileName), showHint],
|
|
331
336
|
};
|
|
332
337
|
}
|
|
333
|
-
const count = { returned: acceptances.length, complete: true };
|
|
334
338
|
if (!acceptances.length) {
|
|
335
339
|
return {
|
|
336
340
|
agreementAcceptances: acceptances,
|
|
337
|
-
|
|
341
|
+
...listTotals(acceptances.length, result.total, "agreement acceptances", true),
|
|
342
|
+
complete: true,
|
|
338
343
|
help: ["0 agreement acceptances matched; nobody recorded an acceptance in scope yet, or no row passed --filter"],
|
|
339
344
|
};
|
|
340
345
|
}
|
|
341
|
-
return { agreementAcceptances: acceptances,
|
|
346
|
+
return { agreementAcceptances: acceptances, ...listTotals(acceptances.length, result.total, "agreement acceptances", true), complete: true, help: [...truncationHints, showHint] };
|
|
342
347
|
}
|
|
343
348
|
async function showAcceptanceById(session, flags, profile, operation, help, profileName, command, agreement) {
|
|
344
349
|
const { select, fields } = selectedFields(flags, KNOWN_ACCEPTANCES, KNOWN_ACCEPTANCE_FIELDS, DEFAULT_ACCEPTANCE_SELECT, help, "agreement-acceptance");
|
package/dist/graph-session.js
CHANGED
|
@@ -25,6 +25,7 @@ const READ_SCOPES = new Set([
|
|
|
25
25
|
"AdministrativeUnit.Read.All",
|
|
26
26
|
"Application.Read.All",
|
|
27
27
|
"AuditLog.Read.All",
|
|
28
|
+
"AuthenticationContext.Read.All",
|
|
28
29
|
"CustomSecAttributeDefinition.Read.All",
|
|
29
30
|
"CrossTenantInformation.ReadBasic.All",
|
|
30
31
|
"DelegatedAdminRelationship.Read.All",
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { lstatSync, readFileSync } from "node:fs";
|
|
2
2
|
import { ConfidentialClientApplication } from "@azure/msal-node";
|
|
3
|
+
import { AxiError } from "axi-sdk-js";
|
|
3
4
|
import { graphAudience } from "./app-auth.js";
|
|
4
5
|
// Only this adapter touches application credentials; keys and assertions are referenced, never logged.
|
|
5
6
|
export class MsalApplicationProvider {
|
|
6
7
|
storage = "session-only";
|
|
7
8
|
async acquire(profile, scopes) {
|
|
9
|
+
const ref = profile.credentialRef;
|
|
8
10
|
const configuration = {
|
|
9
11
|
auth: { clientId: profile.clientId, authority: `https://login.microsoftonline.com/${profile.tenantId}`,
|
|
10
|
-
...(
|
|
11
|
-
? {
|
|
12
|
-
:
|
|
12
|
+
...(ref.provider === "client-secret"
|
|
13
|
+
? { clientSecret: this.clientSecret(ref) }
|
|
14
|
+
: ref.provider === "certificate"
|
|
15
|
+
? { clientCertificate: { thumbprint: ref.thumbprint, privateKey: await this.privateKey(ref) } }
|
|
16
|
+
: { clientAssertion: () => this.assertion(ref) }) },
|
|
13
17
|
system: { loggerOptions: { loggerCallback: () => { }, piiLoggingEnabled: false } },
|
|
14
18
|
};
|
|
15
19
|
const result = await new ConfidentialClientApplication(configuration)
|
|
@@ -20,23 +24,102 @@ export class MsalApplicationProvider {
|
|
|
20
24
|
throw new Error("Configured application authority mismatch");
|
|
21
25
|
return { token: result.accessToken, expiresAt: result.expiresOn?.getTime() ?? NaN, tenantId: profile.tenantId, clientId: profile.clientId };
|
|
22
26
|
}
|
|
23
|
-
async privateKey(
|
|
27
|
+
async privateKey(ref) {
|
|
28
|
+
if (ref.keyFile !== undefined) {
|
|
29
|
+
this.storage = "session-only";
|
|
30
|
+
return this.normalizeKey(this.readProtectedFile(ref.keyFile), ref.keyFile);
|
|
31
|
+
}
|
|
32
|
+
if (ref.keyEnv !== undefined) {
|
|
33
|
+
this.storage = "session-only";
|
|
34
|
+
const material = process.env[ref.keyEnv];
|
|
35
|
+
if (typeof material !== "string" || !material.trim())
|
|
36
|
+
throw this.missing(ref.keyEnv);
|
|
37
|
+
return this.normalizeKey(material, ref.keyEnv);
|
|
38
|
+
}
|
|
24
39
|
const store = await import("keytar").then(module => module.default).catch(() => undefined);
|
|
25
40
|
this.storage = store ? "os-protected" : "session-only";
|
|
26
|
-
const material = await store?.getPassword("mg-axi", key).catch(() => null);
|
|
41
|
+
const material = await store?.getPassword("mg-axi", ref.key).catch(() => null);
|
|
27
42
|
if (typeof material !== "string" || !material.trim())
|
|
28
43
|
throw new Error("Missing referenced certificate private key");
|
|
29
44
|
return material;
|
|
30
45
|
}
|
|
31
|
-
|
|
46
|
+
clientSecret(ref) {
|
|
32
47
|
this.storage = "session-only";
|
|
33
|
-
|
|
48
|
+
if (ref.secretEnv !== undefined) {
|
|
49
|
+
const secret = process.env[ref.secretEnv];
|
|
50
|
+
if (typeof secret !== "string" || !secret.trim())
|
|
51
|
+
throw this.missing(ref.secretEnv);
|
|
52
|
+
return secret.trim();
|
|
53
|
+
}
|
|
54
|
+
if (ref.secretFile !== undefined)
|
|
55
|
+
return this.readProtectedFile(ref.secretFile);
|
|
56
|
+
throw new Error("Missing client-secret credential holder");
|
|
57
|
+
}
|
|
58
|
+
async assertion(ref) {
|
|
59
|
+
this.storage = "session-only";
|
|
60
|
+
if (ref.tokenFileEnv === undefined) {
|
|
61
|
+
const file = process.env.AZURE_FEDERATED_TOKEN_FILE;
|
|
62
|
+
if (!file?.trim())
|
|
63
|
+
throw new Error("Missing federated token source");
|
|
64
|
+
// Read on every invocation: projected federated tokens rotate on disk.
|
|
65
|
+
const assertion = readFileSync(file, "utf8").trim();
|
|
66
|
+
if (!assertion)
|
|
67
|
+
throw new Error("Missing federated token source");
|
|
68
|
+
return assertion;
|
|
69
|
+
}
|
|
70
|
+
const file = process.env[ref.tokenFileEnv];
|
|
34
71
|
if (!file?.trim())
|
|
35
|
-
throw
|
|
72
|
+
throw this.missing(ref.tokenFileEnv);
|
|
36
73
|
// Read on every invocation: projected federated tokens rotate on disk.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
74
|
+
try {
|
|
75
|
+
const assertion = readFileSync(file, "utf8").trim();
|
|
76
|
+
if (!assertion)
|
|
77
|
+
throw this.missing(ref.tokenFileEnv);
|
|
78
|
+
return assertion;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof AxiError)
|
|
82
|
+
throw error;
|
|
83
|
+
throw this.missing(ref.tokenFileEnv);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Key and secret files hold long-lived material: regular file, owned by the
|
|
87
|
+
// current user, mode 0600, never a symlink. Relative paths resolve against
|
|
88
|
+
// the process working directory at acquire time. Windows skips the
|
|
89
|
+
// ownership and mode checks. Every failure names only the path, never content.
|
|
90
|
+
readProtectedFile(path) {
|
|
91
|
+
try {
|
|
92
|
+
const stat = lstatSync(path);
|
|
93
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
94
|
+
throw this.missing(path);
|
|
95
|
+
if (process.platform !== "win32") {
|
|
96
|
+
if (typeof process.getuid === "function" && stat.uid !== process.getuid())
|
|
97
|
+
throw this.missing(path);
|
|
98
|
+
if ((stat.mode & 0o777) !== 0o600)
|
|
99
|
+
throw this.missing(path);
|
|
100
|
+
}
|
|
101
|
+
const content = readFileSync(path, "utf8").trim();
|
|
102
|
+
if (!content)
|
|
103
|
+
throw this.missing(path);
|
|
104
|
+
return content;
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error instanceof AxiError)
|
|
108
|
+
throw error;
|
|
109
|
+
throw this.missing(path);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// PEM material from files and env vars; env vars may carry escaped "\n".
|
|
113
|
+
normalizeKey(material, reference) {
|
|
114
|
+
const normalized = material.includes("\n") ? material : material.replace(/\\n/g, "\n");
|
|
115
|
+
if (/ENCRYPTED PRIVATE KEY/.test(normalized) || /Proc-Type:\s*4,ENCRYPTED/.test(normalized))
|
|
116
|
+
throw new AxiError(`Application credential reference ${reference} is passphrase-protected; passphrase-protected keys are unsupported`, "AUTH_REQUIRED", ["mg-axi profile show --profile <name>", "Provision an unencrypted PEM through the referenced environment variable or file"]);
|
|
117
|
+
if (!normalized.trim())
|
|
118
|
+
throw this.missing(reference);
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
// Fail-closed holder error: names only the env var name or file path.
|
|
122
|
+
missing(reference) {
|
|
123
|
+
return new AxiError(`Application credential unavailable for the configured reference ${reference}; no user or device-code fallback was attempted`, "AUTH_REQUIRED", ["mg-axi profile show --profile <name>", "Supply the referenced credential through the named environment variable or file"]);
|
|
41
124
|
}
|
|
42
125
|
}
|
package/dist/profiles.js
CHANGED
|
@@ -11,6 +11,9 @@ function invalid(message) {
|
|
|
11
11
|
throw new AxiError(message, "VALIDATION_ERROR", ["mg-axi profile create --help", "Correct the version-1 profile configuration; no automatic migration is performed"]);
|
|
12
12
|
}
|
|
13
13
|
const thumbprint = /^[0-9a-f]{40}$/i;
|
|
14
|
+
const envName = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
15
|
+
const credentialPath = (value) => typeof value === "string" && !!value.trim();
|
|
16
|
+
const exactlyOne = (values) => values.filter(value => value !== undefined).length === 1;
|
|
14
17
|
const sharedKeys = ["mode", "tenantId", "clientId", "cloud", "enabledPacks", "preview", "sensitiveAreas", "credentialRef", "allowDeviceCode", "writes"];
|
|
15
18
|
const MAX_WRITE_OPERATIONS = 64;
|
|
16
19
|
const MAX_WRITE_OPERATION_LENGTH = 256;
|
|
@@ -47,18 +50,35 @@ export function validateApplicationProfile(value) {
|
|
|
47
50
|
const ref = profile.credentialRef;
|
|
48
51
|
const writes = validateWrites(value.writes);
|
|
49
52
|
const writeFields = writes === undefined ? {} : { writes };
|
|
50
|
-
if (ref.provider === "certificate" && exact(ref, ["provider", "key", "thumbprint"]) && typeof ref.key === "string" && guid.test(ref.key) &&
|
|
51
|
-
typeof ref.thumbprint === "string" && thumbprint.test(ref.thumbprint)) {
|
|
53
|
+
if (ref.provider === "certificate" && exact(ref, ["provider", "key", "thumbprint", "keyFile", "keyEnv"]) && typeof ref.key === "string" && guid.test(ref.key) &&
|
|
54
|
+
typeof ref.thumbprint === "string" && thumbprint.test(ref.thumbprint) && ref.keyFile === undefined && ref.keyEnv === undefined) {
|
|
52
55
|
return Object.freeze({ mode: "application", tenantId: profile.tenantId, clientId: profile.clientId, cloud: "commercial",
|
|
53
56
|
preview: profile.preview, allowDeviceCode: false, enabledPacks: Object.freeze([...profile.enabledPacks]), sensitiveAreas: Object.freeze([]),
|
|
54
57
|
credentialRef: Object.freeze({ provider: "certificate", key: ref.key, thumbprint: ref.thumbprint.toLowerCase() }), ...writeFields });
|
|
55
58
|
}
|
|
56
|
-
if (ref.provider === "
|
|
59
|
+
if (ref.provider === "certificate" && exact(ref, ["provider", "key", "thumbprint", "keyFile", "keyEnv"]) && typeof ref.key === "string" && guid.test(ref.key) &&
|
|
60
|
+
typeof ref.thumbprint === "string" && thumbprint.test(ref.thumbprint) && exactlyOne([ref.keyFile, ref.keyEnv]) &&
|
|
61
|
+
(ref.keyFile === undefined || credentialPath(ref.keyFile)) && (ref.keyEnv === undefined || (typeof ref.keyEnv === "string" && envName.test(ref.keyEnv)))) {
|
|
62
|
+
const keySource = ref.keyFile === undefined ? { keyEnv: ref.keyEnv } : { keyFile: ref.keyFile };
|
|
57
63
|
return Object.freeze({ mode: "application", tenantId: profile.tenantId, clientId: profile.clientId, cloud: "commercial",
|
|
58
64
|
preview: profile.preview, allowDeviceCode: false, enabledPacks: Object.freeze([...profile.enabledPacks]), sensitiveAreas: Object.freeze([]),
|
|
59
|
-
credentialRef: Object.freeze({ provider: "
|
|
65
|
+
credentialRef: Object.freeze({ provider: "certificate", key: ref.key, thumbprint: ref.thumbprint.toLowerCase(), ...keySource }), ...writeFields });
|
|
60
66
|
}
|
|
61
|
-
|
|
67
|
+
if (ref.provider === "client-secret" && exact(ref, ["provider", "key", "secretEnv", "secretFile"]) && typeof ref.key === "string" && guid.test(ref.key) &&
|
|
68
|
+
exactlyOne([ref.secretEnv, ref.secretFile]) &&
|
|
69
|
+
(ref.secretEnv === undefined || (typeof ref.secretEnv === "string" && envName.test(ref.secretEnv))) && (ref.secretFile === undefined || credentialPath(ref.secretFile))) {
|
|
70
|
+
const secretSource = ref.secretEnv === undefined ? { secretFile: ref.secretFile } : { secretEnv: ref.secretEnv };
|
|
71
|
+
return Object.freeze({ mode: "application", tenantId: profile.tenantId, clientId: profile.clientId, cloud: "commercial",
|
|
72
|
+
preview: profile.preview, allowDeviceCode: false, enabledPacks: Object.freeze([...profile.enabledPacks]), sensitiveAreas: Object.freeze([]),
|
|
73
|
+
credentialRef: Object.freeze({ provider: "client-secret", key: ref.key, ...secretSource }), ...writeFields });
|
|
74
|
+
}
|
|
75
|
+
if (ref.provider === "federated" && exact(ref, ["provider", "key", "tokenFileEnv"]) && typeof ref.key === "string" && guid.test(ref.key) &&
|
|
76
|
+
(ref.tokenFileEnv === undefined || (typeof ref.tokenFileEnv === "string" && envName.test(ref.tokenFileEnv)))) {
|
|
77
|
+
return Object.freeze({ mode: "application", tenantId: profile.tenantId, clientId: profile.clientId, cloud: "commercial",
|
|
78
|
+
preview: profile.preview, allowDeviceCode: false, enabledPacks: Object.freeze([...profile.enabledPacks]), sensitiveAreas: Object.freeze([]),
|
|
79
|
+
credentialRef: Object.freeze({ provider: "federated", key: ref.key, ...(ref.tokenFileEnv === undefined ? {} : { tokenFileEnv: ref.tokenFileEnv }) }), ...writeFields });
|
|
80
|
+
}
|
|
81
|
+
invalid("Invalid application profile; credentialRef must be a certificate, client-secret or federated reference, never inlined key material");
|
|
62
82
|
}
|
|
63
83
|
export function validateProfile(value) {
|
|
64
84
|
if (!!value && typeof value === "object" && !Array.isArray(value) && value.mode === "application")
|
|
@@ -107,15 +127,41 @@ export class Profiles {
|
|
|
107
127
|
invalid("Profile name must contain only letters, digits, underscores or hyphens");
|
|
108
128
|
const certificate = application?.certificateThumbprint;
|
|
109
129
|
const federated = application?.federated ?? false;
|
|
130
|
+
const secretEnv = application?.clientSecretEnv;
|
|
131
|
+
const secretFile = application?.clientSecretFile;
|
|
132
|
+
const keyFile = application?.certificateKeyFile;
|
|
133
|
+
const keyEnv = application?.certificateKeyEnv;
|
|
134
|
+
const tokenFileEnv = application?.federatedTokenFileEnv;
|
|
110
135
|
if (application && (certificate !== undefined && (typeof certificate !== "string" || !/^[0-9a-f]{40}$/i.test(certificate))))
|
|
111
|
-
invalid("Application certificate profiles name a 40-hex-digit thumbprint; the private key stays in
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
invalid("Application certificate profiles name a 40-hex-digit thumbprint; the private key stays in referenced storage");
|
|
137
|
+
for (const [label, value] of [["--client-secret-env", secretEnv], ["--certificate-key-env", keyEnv], ["--federated-token-file-env", tokenFileEnv]]) {
|
|
138
|
+
if (application && value !== undefined && (typeof value !== "string" || !envName.test(value)))
|
|
139
|
+
invalid(`Application profiles name the credential holder with ${label}; names match /^[A-Za-z_][A-Za-z0-9_]*$/`);
|
|
140
|
+
}
|
|
141
|
+
for (const [label, value] of [["--client-secret-file", secretFile], ["--certificate-key-file", keyFile]]) {
|
|
142
|
+
if (application && value !== undefined && !credentialPath(value))
|
|
143
|
+
invalid(`Application profiles name the credential holder with ${label}; paths must be nonempty`);
|
|
144
|
+
}
|
|
145
|
+
if (application && secretEnv !== undefined && secretFile !== undefined)
|
|
146
|
+
invalid("Application client-secret profiles use exactly one holder: --client-secret-env or --client-secret-file");
|
|
147
|
+
if (application && keyFile !== undefined && keyEnv !== undefined)
|
|
148
|
+
invalid("Application certificate profiles use exactly one key holder: --certificate-key-file or --certificate-key-env; omit both for the OS keychain");
|
|
149
|
+
if (application && (keyFile !== undefined || keyEnv !== undefined) && certificate === undefined)
|
|
150
|
+
invalid("Certificate key holders need --certificate-thumbprint on the same profile");
|
|
151
|
+
if (application && tokenFileEnv !== undefined && !federated)
|
|
152
|
+
invalid("Federated token-file holders need --federated on the same profile");
|
|
153
|
+
if (application && [certificate !== undefined, secretEnv !== undefined || secretFile !== undefined, federated].filter(Boolean).length !== 1)
|
|
154
|
+
invalid("Application profiles use exactly one credential: --certificate-thumbprint, --client-secret-env/--client-secret-file or --federated");
|
|
114
155
|
if (application && allowDeviceCode)
|
|
115
156
|
invalid("Application profiles never use device code; it is a delegated login method");
|
|
157
|
+
const credentialRef = certificate === undefined
|
|
158
|
+
? secretEnv === undefined && secretFile === undefined
|
|
159
|
+
? { provider: "federated", key: randomUUID(), ...(tokenFileEnv === undefined ? {} : { tokenFileEnv }) }
|
|
160
|
+
: { provider: "client-secret", key: randomUUID(), ...(secretEnv === undefined ? { secretFile: secretFile } : { secretEnv }) }
|
|
161
|
+
: { provider: "certificate", key: randomUUID(), thumbprint: certificate, ...(keyFile === undefined && keyEnv === undefined ? {} : keyFile === undefined ? { keyEnv: keyEnv } : { keyFile }) };
|
|
116
162
|
const profile = application
|
|
117
163
|
? validateApplicationProfile({ mode: "application", tenantId, clientId, cloud, enabledPacks: ["entra"], preview: false, sensitiveAreas: [],
|
|
118
|
-
credentialRef
|
|
164
|
+
credentialRef, allowDeviceCode: false })
|
|
119
165
|
: validateDelegatedProfile({ mode: "delegated", tenantId, clientId, cloud, enabledPacks: ["entra"], preview: false, sensitiveAreas: [], credentialRef: { provider: "os-or-session", key: randomUUID() }, allowDeviceCode });
|
|
120
166
|
const config = this.load();
|
|
121
167
|
if (Object.hasOwn(config.profiles, name))
|
package/dist/setup.js
CHANGED
|
@@ -15,7 +15,9 @@ const READ_FAMILY_PREFIXES = [
|
|
|
15
15
|
["entra user", "user"],
|
|
16
16
|
["entra registration", "registration"],
|
|
17
17
|
["entra group", "group"],
|
|
18
|
+
["entra directory-role-template", "directory-role/PIM"],
|
|
18
19
|
["entra directory-role", "directory-role/PIM"],
|
|
20
|
+
["entra scoped-role-membership", "directory-role/PIM"],
|
|
19
21
|
["entra role-assignment", "directory-role/PIM"],
|
|
20
22
|
["entra pim", "directory-role/PIM"],
|
|
21
23
|
["entra device", "device"],
|
|
@@ -49,6 +51,7 @@ const READ_FAMILY_PREFIXES = [
|
|
|
49
51
|
["entra tenant-information", "tenant information"],
|
|
50
52
|
["entra sign-in", "sign-in/directory-audit"],
|
|
51
53
|
["entra directory-audit", "sign-in/directory-audit"],
|
|
54
|
+
["entra provisioning", "sign-in/directory-audit"],
|
|
52
55
|
["entra application", "application/service-principal and consent grants"],
|
|
53
56
|
["entra service-principal", "application/service-principal and consent grants"],
|
|
54
57
|
["entra risky-user", "risk"],
|
|
@@ -102,7 +105,7 @@ export function setupView(store) {
|
|
|
102
105
|
"node dist/bin/mg-axi.js --version",
|
|
103
106
|
],
|
|
104
107
|
guidance: [
|
|
105
|
-
`Hand-edit ${store.path} or set MG_AXI_CONFIG to a separate configuration file;
|
|
108
|
+
`Hand-edit ${store.path} or set MG_AXI_CONFIG to a separate configuration file; a profile stores only a credential reference - an OS keychain entry, an environment variable name, or a file path - and never the secret value, never in argv or config`,
|
|
106
109
|
"node dist/bin/mg-axi.js profile create --name soc --tenant <tenant-id> --client <client-id> --cloud commercial",
|
|
107
110
|
"node dist/bin/mg-axi.js login --profile soc --scopes https://graph.microsoft.com/User.Read.All",
|
|
108
111
|
"node dist/bin/mg-axi.js doctor",
|
|
@@ -112,7 +115,7 @@ export function setupView(store) {
|
|
|
112
115
|
reads,
|
|
113
116
|
writes,
|
|
114
117
|
local: implemented.length - reads - writes,
|
|
115
|
-
api: `Entra ${families.slice(0, -1).join(", ")} and ${families.at(-1)} reads plus reviewed raw api get, one gated group-membership write, one gated account enable/disable write, one gated session-revocation write and one gated risky-user dismissal write; every other operation is scheduled, blocked, deprecated or excluded`,
|
|
118
|
+
api: `Entra ${families.slice(0, -1).join(", ")} and ${families.at(-1)} reads plus reviewed raw api get, one gated group-membership write, one gated account enable/disable write, one gated session-revocation write and one gated risky-user dismissal write; every other v1.0 operation is deferred out of v1, and every other beta operation remains scheduled (never agreed for v1), blocked, deprecated, unavailable or excluded`,
|
|
116
119
|
report: "docs/coverage.md",
|
|
117
120
|
},
|
|
118
121
|
integration: "Install skills/mg-axi/SKILL.md explicitly through your agent's skill installation mechanism; setup only shows guidance and installs no skills or hooks",
|