From 530b033af1be01bdc0796bc3a3273e5726fe461b Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 28 Mar 2025 14:49:20 +0100 Subject: [PATCH] [socket organization policy license] Add license policy cmd --- .../cmd-organization-policy-license.test.ts | 102 ++++++++++++++++++ .../cmd-organization-policy-license.ts | 93 ++++++++++++++++ .../cmd-organization-policy-security.test.ts | 2 +- .../organization/cmd-organization-policy.ts | 4 +- .../organization/fetch-license-policy.ts | 45 ++++++++ .../organization/fetch-security-policy.ts | 4 +- .../organization/get-license-policy.ts | 102 ------------------ .../organization/handle-license-policy.ts | 14 +++ .../organization/handle-security-policy.ts | 4 +- .../organization/output-license-policy.ts | 41 +++++++ .../organization/output-security-policy.ts | 2 +- 11 files changed, 304 insertions(+), 109 deletions(-) create mode 100644 src/commands/organization/cmd-organization-policy-license.test.ts create mode 100644 src/commands/organization/cmd-organization-policy-license.ts create mode 100644 src/commands/organization/fetch-license-policy.ts delete mode 100644 src/commands/organization/get-license-policy.ts create mode 100644 src/commands/organization/handle-license-policy.ts create mode 100644 src/commands/organization/output-license-policy.ts diff --git a/src/commands/organization/cmd-organization-policy-license.test.ts b/src/commands/organization/cmd-organization-policy-license.test.ts new file mode 100644 index 000000000..c10d3847b --- /dev/null +++ b/src/commands/organization/cmd-organization-policy-license.test.ts @@ -0,0 +1,102 @@ +import path from 'node:path' + +import { describe, expect } from 'vitest' + +import constants from '../../../dist/constants.js' +import { cmdit, invokeNpm } from '../../../test/utils' + +const { CLI } = constants + +describe('socket organization policy license', async () => { + // Lazily access constants.rootBinPath. + const entryPath = path.join(constants.rootBinPath, `${CLI}.js`) + + cmdit( + ['organization', 'policy', 'license', '--help', '--config', '{}'], + 'should support --help', + async cmd => { + const { code, stderr, stdout } = await invokeNpm(entryPath, cmd) + expect(stdout).toMatchInlineSnapshot( + ` + "Retrieve the license policy of an organization. + + Usage + $ socket organization policy license + + Options + --dryRun Do input validation for a command and exit 0 when input is ok + --help Print this help. + --json Output result as json + --markdown Output result as markdown + + Your API token will need the \`license-policy:read\` permission otherwise + the request will fail with an authentication error. + + Examples + $ socket organization policy license mycorp + $ socket organization policy license mycorp --json" + ` + ) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | Socket.dev CLI ver + |__ | . | _| '_| -_| _| | Node: , API token set: + |_____|___|___|_,_|___|_|.dev | Command: \`socket organization policy license\`, cwd: " + `) + + expect(code, 'help should exit with code 2').toBe(2) + expect(stderr, 'banner includes base command').toContain( + '`socket organization policy license`' + ) + } + ) + + cmdit( + ['organization', 'policy', 'license', '--dry-run', '--config', '{}'], + 'should reject dry run without proper args', + async cmd => { + const { code, stderr, stdout } = await invokeNpm(entryPath, cmd) + expect(stdout).toMatchInlineSnapshot(`""`) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | Socket.dev CLI ver + |__ | . | _| '_| -_| _| | Node: , API token set: + |_____|___|___|_,_|___|_|.dev | Command: \`socket organization policy license\`, cwd: + + \\x1b[31m\\xd7\\x1b[39m \\x1b[41m\\x1b[1m\\x1b[37m Input error: \\x1b[39m\\x1b[22m\\x1b[49m \\x1b[1mPlease review the input requirements and try again\\x1b[22m: + + - Org name as the first argument (\\x1b[31mmissing\\x1b[39m)" + `) + + expect(code, 'dry-run should exit with code 2 if input bad').toBe(2) + } + ) + + cmdit( + [ + 'organization', + 'policy', + 'license', + 'fakeorg', + '--dry-run', + '--config', + '{}' + ], + 'should be ok with org name and id', + async cmd => { + const { code, stderr, stdout } = await invokeNpm(entryPath, cmd) + expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | Socket.dev CLI ver + |__ | . | _| '_| -_| _| | Node: , API token set: + |_____|___|___|_,_|___|_|.dev | Command: \`socket organization policy license\`, cwd: " + `) + + expect(code, 'dry-run should exit with code 0 if input ok').toBe(0) + } + ) +}) diff --git a/src/commands/organization/cmd-organization-policy-license.ts b/src/commands/organization/cmd-organization-policy-license.ts new file mode 100644 index 000000000..96a81b5e2 --- /dev/null +++ b/src/commands/organization/cmd-organization-policy-license.ts @@ -0,0 +1,93 @@ +import { logger } from '@socketsecurity/registry/lib/logger' + +import { handleLicensePolicy } from './handle-license-policy' +import constants from '../../constants' +import { commonFlags, outputFlags } from '../../flags' +import { getConfigValue } from '../../utils/config' +import { handleBadInput } from '../../utils/handle-bad-input' +import { meowOrExit } from '../../utils/meow-with-subcommands' +import { getFlagListOutput } from '../../utils/output-formatting' + +import type { CliCommandConfig } from '../../utils/meow-with-subcommands' + +const { DRY_RUN_BAIL_TEXT } = constants + +// TODO: secret toplevel alias `socket license policy`? +const config: CliCommandConfig = { + commandName: 'license', + description: 'Retrieve the license policy of an organization.', + hidden: true, + flags: { + ...commonFlags, + ...outputFlags + }, + help: (command, _config) => ` + Usage + $ ${command} + + Options + ${getFlagListOutput(config.flags, 6)} + + Your API token will need the \`license-policy:read\` permission otherwise + the request will fail with an authentication error. + + Examples + $ ${command} mycorp + $ ${command} mycorp --json + ` +} + +export const cmdOrganizationPolicyLicense = { + description: config.description, + hidden: config.hidden, + run +} + +async function run( + argv: string[] | readonly string[], + importMeta: ImportMeta, + { parentName }: { parentName: string } +): Promise { + const cli = meowOrExit({ + argv, + config, + importMeta, + parentName + }) + + const json = Boolean(cli.flags['json']) + const markdown = Boolean(cli.flags['markdown']) + + const defaultOrgSlug = getConfigValue('defaultOrg') + const orgSlug = defaultOrgSlug || cli.input[0] || '' + + const wasBadInput = handleBadInput( + { + hide: defaultOrgSlug, + test: orgSlug, + message: 'Org name as the first argument', + pass: 'ok', + fail: 'missing' + }, + { + hide: !json || !markdown, + test: !json || !markdown, + message: 'The json and markdown flags cannot be both set, pick one', + pass: 'ok', + fail: 'omit one' + } + ) + if (wasBadInput) { + return + } + + if (cli.flags['dryRun']) { + logger.log(DRY_RUN_BAIL_TEXT) + return + } + + await handleLicensePolicy( + orgSlug, + json ? 'json' : markdown ? 'markdown' : 'text' + ) +} diff --git a/src/commands/organization/cmd-organization-policy-security.test.ts b/src/commands/organization/cmd-organization-policy-security.test.ts index f748ef6f8..d82c37bdd 100644 --- a/src/commands/organization/cmd-organization-policy-security.test.ts +++ b/src/commands/organization/cmd-organization-policy-security.test.ts @@ -7,7 +7,7 @@ import { cmdit, invokeNpm } from '../../../test/utils' const { CLI } = constants -describe('socket organization list', async () => { +describe('socket organization policy security', async () => { // Lazily access constants.rootBinPath. const entryPath = path.join(constants.rootBinPath, `${CLI}.js`) diff --git a/src/commands/organization/cmd-organization-policy.ts b/src/commands/organization/cmd-organization-policy.ts index 06a2c775d..66e75c852 100644 --- a/src/commands/organization/cmd-organization-policy.ts +++ b/src/commands/organization/cmd-organization-policy.ts @@ -1,3 +1,4 @@ +import { cmdOrganizationPolicyLicense } from './cmd-organization-policy-license' import { cmdOrganizationPolicyPolicy } from './cmd-organization-policy-security' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' @@ -15,7 +16,8 @@ export const cmdOrganizationPolicy: CliSubcommand = { async run(argv, importMeta, { parentName }) { await meowWithSubcommands( { - security: cmdOrganizationPolicyPolicy + security: cmdOrganizationPolicyPolicy, + license: cmdOrganizationPolicyLicense }, { argv, diff --git a/src/commands/organization/fetch-license-policy.ts b/src/commands/organization/fetch-license-policy.ts new file mode 100644 index 000000000..c3206625e --- /dev/null +++ b/src/commands/organization/fetch-license-policy.ts @@ -0,0 +1,45 @@ +import constants from '../../constants' +import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function fetchLicensePolicy( + orgSlug: string +): Promise['data'] | undefined> { + const apiToken = getDefaultToken() + if (!apiToken) { + throw new AuthError( + 'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.' + ) + } + + return await fetchLicensePolicyWithToken(apiToken, orgSlug) +} + +async function fetchLicensePolicyWithToken( + apiToken: string, + orgSlug: string +): Promise['data'] | undefined> { + // Lazily access constants.spinner. + const { spinner } = constants + + const sockSdk = await setupSdk(apiToken) + + spinner.start('Fetching organization license policy...') + + const result = await handleApiCall( + sockSdk.getOrgLicensePolicy(orgSlug), + 'looking up organization quota' + ) + + spinner.successAndStop('Received organization license policy response.') + + if (!result.success) { + handleUnsuccessfulApiResponse('getOrgLicensePolicy', result) + return + } + + return result.data +} diff --git a/src/commands/organization/fetch-security-policy.ts b/src/commands/organization/fetch-security-policy.ts index 9532a901b..e9abe2010 100644 --- a/src/commands/organization/fetch-security-policy.ts +++ b/src/commands/organization/fetch-security-policy.ts @@ -27,14 +27,14 @@ async function fetchSecurityPolicyWithToken( const sockSdk = await setupSdk(apiToken) - spinner.start('Fetching organization quota...') + spinner.start('Fetching organization security policy...') const result = await handleApiCall( sockSdk.getOrgSecurityPolicy(orgSlug), 'looking up organization quota' ) - spinner?.successAndStop('Received organization quota response.') + spinner?.successAndStop('Received organization security policy response.') if (!result.success) { handleUnsuccessfulApiResponse('getOrgSecurityPolicy', result) diff --git a/src/commands/organization/get-license-policy.ts b/src/commands/organization/get-license-policy.ts deleted file mode 100644 index 3ff240cdb..000000000 --- a/src/commands/organization/get-license-policy.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { logger } from '@socketsecurity/registry/lib/logger' - -import constants from '../../constants' -import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { AuthError } from '../../utils/errors' -import { getDefaultToken, setupSdk } from '../../utils/sdk' - -export async function getLicensePolicy( - orgSlug: string, - format: 'text' | 'json' | 'markdown' -): Promise { - const apiToken = getDefaultToken() - if (!apiToken) { - throw new AuthError( - 'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.' - ) - } - await getLicensePolicyWithToken(apiToken, orgSlug, format) -} - -async function getLicensePolicyWithToken( - apiToken: string, - orgSlug: string, - format: 'text' | 'json' | 'markdown' -) { - // Lazily access constants.spinner. - const { spinner } = constants - - spinner.start('Fetching organization quota...') - - const sockSdk = await setupSdk(apiToken) - const result = await handleApiCall( - // sockSdk.getOrgLicensePolicy(orgSlug), - sockSdk.getOrgSecurityPolicy(orgSlug), - "looking up organization's license policy" - ) - - if (!result.success) { - // handleUnsuccessfulApiResponse('getOrgLicensePolicy', result) - handleUnsuccessfulApiResponse('getOrgSecurityPolicy', result) - return - } - - spinner.stop() - - switch (format) { - case 'json': { - logger.log(JSON.stringify(result.data, null, 2)) - return - } - default: { - // logger.log('# License policy\n') - // logger.log( - // `These are the license policies set up for the requested organization:\n` - // ) - // const data = result.data - // const rules = data.securityPolicyRules - // const entries: Array< - // [string, { action: 'defer' | 'error' | 'warn' | 'monitor' | 'ignore' | undefined}] - // > = Object.entries(rules) - // const mapped: Array<[string, string]> = entries.map(([key, value]) => [ - // key, - // value.action - // ]) - // mapped.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) - // logger.log(mdTable(mapped, ['name', 'action'])) - } - } -} - -// function mdTable( -// arr: Array<[string, string]>, -// // This is saying "an array of strings and the strings are a valid key of elements of T" -// // In turn, T is defined above as the audit log event type from our OpenAPI docs. -// cols: string[] -// ): string { -// // Max col width required to fit all data in that column -// const cws = cols.map(col => col.length) -// -// for (const [key, val] of arr) { -// cws[0] = Math.max(cws[0] ?? 0, String(key).length) -// cws[1] = Math.max(cws[1] ?? 0, String(val ?? '').length) -// } -// -// let div = '|' -// for (const cw of cws) div += ' ' + '-'.repeat(cw) + ' |' -// -// let header = '|' -// for (let i = 0; i < cols.length; ++i) { -// header += ' ' + String(cols[i]).padEnd(cws[i] ?? 0, ' ') + ' |' -// } -// -// let body = '' -// for (const [key, val] of arr) { -// body += '|' -// body += ' ' + String(key).padEnd(cws[0] ?? 0, ' ') + ' |' -// body += ' ' + String(val ?? '').padEnd(cws[1] ?? 0, ' ') + ' |' -// body += '\n' -// } -// -// return [div, header, div, body.trim(), div].filter(s => !!s.trim()).join('\n') -// } diff --git a/src/commands/organization/handle-license-policy.ts b/src/commands/organization/handle-license-policy.ts new file mode 100644 index 000000000..1987a33e6 --- /dev/null +++ b/src/commands/organization/handle-license-policy.ts @@ -0,0 +1,14 @@ +import { fetchLicensePolicy } from './fetch-license-policy' +import { outputLicensePolicy } from './output-license-policy' + +export async function handleLicensePolicy( + orgSlug: string, + outputKind: 'text' | 'json' | 'markdown' +): Promise { + const data = await fetchLicensePolicy(orgSlug) + if (!data) { + return + } + + await outputLicensePolicy(data, outputKind) +} diff --git a/src/commands/organization/handle-security-policy.ts b/src/commands/organization/handle-security-policy.ts index d4fc47992..9a95be72b 100644 --- a/src/commands/organization/handle-security-policy.ts +++ b/src/commands/organization/handle-security-policy.ts @@ -1,5 +1,5 @@ import { fetchSecurityPolicy } from './fetch-security-policy' -import { getSecurityPolicy } from './output-security-policy' +import { outputSecurityPolicy } from './output-security-policy' export async function handleSecurityPolicy( orgSlug: string, @@ -10,5 +10,5 @@ export async function handleSecurityPolicy( return } - await getSecurityPolicy(data, outputKind) + await outputSecurityPolicy(data, outputKind) } diff --git a/src/commands/organization/output-license-policy.ts b/src/commands/organization/output-license-policy.ts new file mode 100644 index 000000000..659e7d7bb --- /dev/null +++ b/src/commands/organization/output-license-policy.ts @@ -0,0 +1,41 @@ +import { logger } from '@socketsecurity/registry/lib/logger' + +import { mdTableOfPairs } from '../../utils/markdown' + +import type { SocketSdkReturnType } from '@socketsecurity/sdk' + +export async function outputLicensePolicy( + data: SocketSdkReturnType<'getOrgLicensePolicy'>['data'], + outputKind: 'text' | 'json' | 'markdown' +): Promise { + if (outputKind === 'json') { + let json + try { + json = JSON.stringify(data, null, 2) + } catch { + console.error( + 'Failed to convert the server response to json, try running the same command without --json' + ) + return + } + + logger.log(json) + logger.log('') + return + } + + logger.error('Use --json to get the full result') + logger.log('# License policy') + logger.log('') + logger.log('This is the license policy for your organization:') + logger.log('') + const rules = data.license_policy + // @ts-ignore -- not sure what it's complaining about + const entries = Object.entries(rules) + const mapped: Array<[string, string]> = entries.map( + ([key, value]) => [key, value.allowed ? ' yes' : ' no'] as const + ) + mapped.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) + logger.log(mdTableOfPairs(mapped, ['License Name', 'Allowed'])) + logger.log('') +} diff --git a/src/commands/organization/output-security-policy.ts b/src/commands/organization/output-security-policy.ts index 1119580d5..6cf1f3f6e 100644 --- a/src/commands/organization/output-security-policy.ts +++ b/src/commands/organization/output-security-policy.ts @@ -4,7 +4,7 @@ import { mdTableOfPairs } from '../../utils/markdown' import type { SocketSdkReturnType } from '@socketsecurity/sdk' -export async function getSecurityPolicy( +export async function outputSecurityPolicy( data: SocketSdkReturnType<'getOrgSecurityPolicy'>['data'], outputKind: 'text' | 'json' | 'markdown' ): Promise {