#!/usr/bin/env node // SPDX-License-Identifier: MIT import { parseArgs } from "node:util"; import jmespath from "jmespath"; import { minimatch } from "minimatch"; import { loadPublicConfig } from "./index.js"; import { getGraphClient } from "./graph/auth.js"; import { listApps, listAppPermissions, listAppPermissionsResolved, listAppGrants, listResourcePermissions, } from "./graph/app.js"; import { toMarkdownTable } from "./markdown.js"; function usage() { return `Usage: sk-az-tools [options] Commands: list-apps [--display-name|-n ] list-app-permissions --app-id|-i [--resolve|-r] [--short|-s] [--filter|-f ] list-app-grants --app-id|-i list-resource-permissions [--app-id|-i | --display-name|-n ] table [--header|-H ] Options: -n, --display-name Filter apps by exact display name -i, --app-id Application (client) ID -r, --resolve Resolve permission GUIDs to human-readable values -s, --short Makes output more compact -f, --filter Filter by permission name glob -q, --query Filter output JSON using JMESPath -H, --header Header mode/spec: auto|a OR "col1, col2" OR "key1: Label 1, key2: Label 2" -o, --output Output format: json|j|table|t|alignedtable|at|prettytable|pt (default: json) -h, --help Show this help message`; } function outputFiltered(object, query) { return query ? jmespath.search(object, query) : object; } function parseHeaderSpec(headerValue) { if (!headerValue) { return { mode: "default" }; } const raw = headerValue.trim(); if (raw === "" || raw.toLowerCase() === "auto" || raw.toLowerCase() === "a") { return { mode: "auto" }; } const parts = raw.split(",").map((p) => p.trim()).filter(Boolean); const isMap = parts.some((p) => p.includes(":")); if (!isMap) { return { mode: "list", labels: parts }; } const map = {}; for (const part of parts) { const idx = part.indexOf(":"); if (idx < 0) { throw new Error(`Invalid --header mapping segment: '${part}'`); } const key = part.slice(0, idx).trim(); const label = part.slice(idx + 1).trim(); if (!key || !label) { throw new Error(`Invalid --header mapping segment: '${part}'`); } map[key] = label; } return { mode: "map", map }; } function normalizeOutputFormat(outputValue) { const raw = (outputValue ?? "json").toLowerCase(); if (raw === "json" || raw === "j") return "json"; if (raw === "table" || raw === "t") return "table"; if (raw === "alignedtable" || raw === "at") return "alignedtable"; if (raw === "prettytable" || raw === "pt") return "prettytable"; throw new Error("--output must be one of: json|j, table|t, alignedtable|at, prettytable|pt"); } function omitPermissionGuidColumns(value) { if (Array.isArray(value)) { return value.map((item) => omitPermissionGuidColumns(item)); } if (!value || typeof value !== "object") { return value; } const { resourceAppId, permissionId, ...rest } = value; return rest; } async function readJsonFromStdin() { const input = await new Promise((resolve, reject) => { let data = ""; process.stdin.setEncoding("utf8"); process.stdin.on("data", (chunk) => { data += chunk; }); process.stdin.on("end", () => { resolve(data); }); process.stdin.on("error", (err) => { reject(err); }); }); if (!input.trim()) { throw new Error("No JSON input provided on stdin"); } try { return JSON.parse(input); } catch (err) { throw new Error(`Invalid JSON input on stdin: ${err.message}`); } } async function main() { const argv = process.argv.slice(2); const command = argv[0]; if (command === "-h" || command === "--help") { console.log(usage()); process.exit(0); } const { values } = parseArgs({ args: argv.slice(1), options: { help: { type: "boolean", short: "h" }, "display-name": { type: "string", short: "n" }, "app-id": { type: "string", short: "i" }, resolve: { type: "boolean", short: "r" }, short: { type: "boolean", short: "s" }, filter: { type: "string", short: "f" }, query: { type: "string", short: "q" }, header: { type: "string", short: "H" }, output: { type: "string", short: "o" }, }, strict: true, allowPositionals: false, }); if (values.help || !command) { console.log(usage()); process.exit(0); } const outputFormat = normalizeOutputFormat(values.output); let result; switch (command) { case "table": result = await readJsonFromStdin(); break; case "list-apps": { const config = await loadPublicConfig(); const { client } = await getGraphClient({ tenantId: config.tenantId, clientId: config.clientId, }); result = await listApps(client, { displayName: values["display-name"], }); } break; case "list-app-permissions": if (!values["app-id"]) { throw new Error("--app-id is required for list-app-permissions"); } { const config = await loadPublicConfig(); const { client } = await getGraphClient({ tenantId: config.tenantId, clientId: config.clientId, }); result = values.resolve || values.filter ? await listAppPermissionsResolved(client, values["app-id"]) : await listAppPermissions(client, values["app-id"]); if (values.filter) { const pattern = values.filter; result = result.filter((item) => minimatch(item.permissionValue ?? "", pattern, { nocase: true }) || minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }) ); } } break; case "list-app-grants": if (!values["app-id"]) { throw new Error("--app-id is required for list-app-grants"); } { const config = await loadPublicConfig(); const { client } = await getGraphClient({ tenantId: config.tenantId, clientId: config.clientId, }); result = await listAppGrants(client, values["app-id"]); } break; case "list-resource-permissions": if (!values["app-id"] && !values["display-name"]) { throw new Error("--app-id or --display-name is required for list-resource-permissions"); } if (values["app-id"] && values["display-name"]) { throw new Error("Use either --app-id or --display-name for list-resource-permissions, not both"); } { const config = await loadPublicConfig(); const { client } = await getGraphClient({ tenantId: config.tenantId, clientId: config.clientId, }); result = await listResourcePermissions(client, { appId: values["app-id"], displayName: values["display-name"], }); if (values.filter) { const pattern = values.filter; result = result.filter((item) => minimatch(item.permissionValue ?? "", pattern, { nocase: true }) || minimatch(item.permissionDisplayName ?? "", pattern, { nocase: true }) ); } } break; default: throw new Error(`Unknown command: ${command}`); } const filtered = outputFiltered(result, values.query); const output = command === "list-app-permissions" && values.short ? omitPermissionGuidColumns(filtered) : filtered; const headerSpec = parseHeaderSpec(values.header); if (command === "table") { console.log(toMarkdownTable( output, outputFormat === "alignedtable" || outputFormat === "prettytable", outputFormat === "prettytable", headerSpec, )); } else if (outputFormat === "alignedtable") { console.log(toMarkdownTable(output, true, false, headerSpec)); } else if (outputFormat === "prettytable") { console.log(toMarkdownTable(output, true, true, headerSpec)); } else if (outputFormat === "table") { console.log(toMarkdownTable(output, false, false, headerSpec)); } else { console.log(JSON.stringify(output, null, 2)); } } main().catch((err) => { console.error(`Error: ${err.message}`); console.error(usage()); process.exit(1); });