diff --git a/src/cli.js b/src/cli.js index a2fefbf..8b29f6e 100755 --- a/src/cli.js +++ b/src/cli.js @@ -4,6 +4,7 @@ import { parseArgs } from "node:util"; import jmespath from "jmespath"; +import { minimatch } from "minimatch"; import { loadPublicConfig } from "./index.js"; import { getGraphClient } from "./graph/auth.js"; @@ -12,6 +13,7 @@ import { listAppPermissions, listAppPermissionsResolved, listAppGrants, + listResourcePermissions, } from "./graph/app.js"; import { toMarkdownTable } from "./markdown.js"; @@ -20,20 +22,20 @@ function usage() { Commands: list-apps [--display-name|-n ] - list-app-permissions --app-id|-i [--resolve|-r] [--short|-s] + list-app-permissions --app-id|-i [--resolve|-r] [--short|-s] [--filter|-f ] list-app-grants --app-id|-i - table [--pretty|-p] [--quote-guids|-g] [--header|-H ] + 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 - -p, --pretty Use normalized column widths for Markdown table output - -g, --quote-guids In pretty tables, wrap GUID values in backticks -H, --header Header mode/spec: auto|a OR "col1, col2" OR "key1: Label 1, key2: Label 2" - -o, --output Output format: json|table|prettytable (default: json) + -o, --output Output format: json|j|table|t|alignedtable|at|prettytable|pt (default: json) -h, --help Show this help message`; } @@ -77,6 +79,15 @@ function parseHeaderSpec(headerValue) { 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)); @@ -129,9 +140,8 @@ async function main() { "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" }, - pretty: { type: "boolean", short: "p" }, - "quote-guids": { type: "boolean", short: "g" }, header: { type: "string", short: "H" }, output: { type: "string", short: "o" }, }, @@ -143,10 +153,7 @@ async function main() { console.log(usage()); process.exit(0); } - const outputFormat = values.output ?? "json"; - if (outputFormat !== "json" && outputFormat !== "table" && outputFormat !== "prettytable") { - throw new Error("--output must be one of: json, table, prettytable"); - } + const outputFormat = normalizeOutputFormat(values.output); let result; switch (command) { @@ -175,9 +182,16 @@ async function main() { tenantId: config.tenantId, clientId: config.clientId, }); - result = values.resolve + 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": @@ -193,6 +207,32 @@ async function main() { 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}`); } @@ -205,17 +245,14 @@ async function main() { if (command === "table") { console.log(toMarkdownTable( output, - Boolean(values.pretty), - Boolean(values["quote-guids"]), + 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, - Boolean(values["quote-guids"]), - headerSpec, - )); + console.log(toMarkdownTable(output, true, true, headerSpec)); } else if (outputFormat === "table") { console.log(toMarkdownTable(output, false, false, headerSpec)); } else {