From 7e91773c40184441b691963d025053d59a72b36a Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Mon, 3 Mar 2025 15:33:24 +0100 Subject: [PATCH] Implement --json and --markdown for audit-log --- package.json | 4 +- src/commands/audit-log/cmd-audit-log.ts | 31 ++- src/commands/audit-log/get-audit-log.ts | 241 +++++++++++++++++++++--- test/dry-run.test.ts | 2 +- 4 files changed, 226 insertions(+), 52 deletions(-) diff --git a/package.json b/package.json index 502fea113..c16952ed8 100644 --- a/package.json +++ b/package.json @@ -60,10 +60,10 @@ "test": "run-s check test:*", "test:prepare": "cross-env VITEST=1 npm run build", "test:unit": "vitest --run", - "test:unit:update": "vitest --update", + "test:unit:update": "vitest --run --update", "test:unit:coverage": "vitest run --coverage", "test-ci": "run-s test:*", - "testu": "cross-env SOCKET_CLI_NO_API_TOKEN=1 run-s test:prepare test:unit:update", + "testu": "cross-env SOCKET_CLI_NO_API_TOKEN=1 run-s test:prepare; npm run test:unit:update --", "update": "run-p --aggregate-output update:**", "update:deps": "npx --yes npm-check-updates" }, diff --git a/src/commands/audit-log/cmd-audit-log.ts b/src/commands/audit-log/cmd-audit-log.ts index 3c2d017e3..055d21618 100644 --- a/src/commands/audit-log/cmd-audit-log.ts +++ b/src/commands/audit-log/cmd-audit-log.ts @@ -5,10 +5,8 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { getAuditLog } from './get-audit-log' import constants from '../../constants' import { commonFlags, outputFlags } from '../../flags' -import { AuthError } from '../../utils/errors' import { meowOrExit } from '../../utils/meow-with-subcommands' import { getFlagListOutput } from '../../utils/output-formatting' -import { getDefaultToken } from '../../utils/sdk' import type { CliCommandConfig } from '../../utils/meow-with-subcommands' @@ -70,7 +68,9 @@ async function run( parentName }) - const type = String(cli.flags['type'] || '') + const { json, markdown, page, perPage, type } = cli.flags + + const logType = String(type || '') const [orgSlug = ''] = cli.input if (!orgSlug) { @@ -78,8 +78,12 @@ async function run( // options or missing arguments. // https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html process.exitCode = 2 - logger.error(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n - - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n`) + logger.error( + ` + ${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n + - Org name as the first argument ${!orgSlug ? colors.red('(missing!)') : colors.green('(ok)')}\n + `.trim() + ) return } @@ -88,20 +92,11 @@ async function run( return } - 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 getAuditLog({ - apiToken, orgSlug, - outputJson: Boolean(cli.flags['json']), - outputMarkdown: Boolean(cli.flags['markdown']), - page: Number(cli.flags['page'] || 0), - perPage: Number(cli.flags['perPage'] || 0), - type: type.charAt(0).toUpperCase() + type.slice(1) + outputKind: json ? 'json' : markdown ? 'markdown' : 'print', + page: Number(page || 0), + perPage: Number(perPage || 0), + logType: logType.charAt(0).toUpperCase() + logType.slice(1) }) } diff --git a/src/commands/audit-log/get-audit-log.ts b/src/commands/audit-log/get-audit-log.ts index 28fcd9ac9..93e78039a 100644 --- a/src/commands/audit-log/get-audit-log.ts +++ b/src/commands/audit-log/get-audit-log.ts @@ -1,9 +1,11 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { Separator, select } from '@socketsecurity/registry/lib/prompts' +import { SocketSdkReturnType } from '@socketsecurity/sdk' import constants from '../../constants' import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api' -import { setupSdk } from '../../utils/sdk' +import { AuthError } from '../../utils/errors' +import { getDefaultToken, setupSdk } from '../../utils/sdk' import type { Choice } from '@socketsecurity/registry/lib/prompts' @@ -12,51 +14,185 @@ type AuditChoice = Choice type AuditChoices = (Separator | AuditChoice)[] export async function getAuditLog({ - apiToken, + logType, orgSlug, - outputJson, - outputMarkdown, + outputKind, page, - perPage, - type + perPage }: { - apiToken: string - outputJson: boolean - outputMarkdown: boolean + outputKind: 'json' | 'markdown' | 'print' orgSlug: string page: number perPage: number - type: string + logType: string }): Promise { - // Lazily access constants.spinner. - const { spinner } = constants + 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.' + ) + } - spinner.start(`Looking up audit log for ${orgSlug}`) + const auditLogs = await getAuditLogWithToken({ + apiToken, + orgSlug, + outputKind, + page, + perPage, + logType + }) + if (!auditLogs) return - const socketSdk = await setupSdk(apiToken) - const result = await handleApiCall( - socketSdk.getAuditLogEvents(orgSlug, { - outputJson, - outputMarkdown, - orgSlug, - type, - page, - per_page: perPage - }), - `Looking up audit log for ${orgSlug}\n` - ) + if (outputKind === 'json') + await outputAsJson(auditLogs.results, orgSlug, logType, page, perPage) + else if (outputKind === 'markdown') + await outputAsMarkdown(auditLogs.results, orgSlug, logType, page, perPage) + else await outputAsPrint(auditLogs.results, orgSlug, logType) +} - if (!result.success) { - handleUnsuccessfulApiResponse('getAuditLogEvents', result, spinner) +async function outputAsJson( + auditLogs: SocketSdkReturnType<'getAuditLogEvents'>['data']['results'], + orgSlug: string, + logType: string, + page: number, + perPage: number +): Promise { + let json + try { + json = JSON.stringify( + { + desc: 'Audit logs for given query', + generated: new Date().toISOString(), + org: orgSlug, + logType, + page, + perPage, + logs: auditLogs.map(log => { + // Note: The subset is pretty arbitrary + const { + created_at, + event_id, + ip_address, + type, + user_agent, + user_email + } = log + return { + event_id, + created_at, + ip_address, + type, + user_agent, + user_email + } + }) + }, + null, + 2 + ) + } catch (e) { + logger.error( + 'There was a problem converting the logs to JSON, please try without the `--json` flag' + ) + process.exitCode = 1 return } - spinner.stop() + logger.log(json) +} + +async function outputAsMarkdown( + auditLogs: SocketSdkReturnType<'getAuditLogEvents'>['data']['results'], + orgSlug: string, + logType: string, + page: number, + perPage: number +): Promise { + let md + try { + const table = mdTable(auditLogs, [ + 'event_id', + 'created_at', + 'type', + 'user_email', + 'ip_address', + 'user_agent' + ]) + + md = + ` +# Socket Audit Logs + +These are the Socket.dev audit logs as per requested query. +- org: ${orgSlug} +- type filter: ${logType || '(none)'} +- page: ${page} +- per page: ${perPage} +- generated: ${new Date().toISOString()} + +${table} + `.trim() + '\n' + } catch (e) { + logger.error( + 'There was a problem converting the logs to JSON, please try without the `--json` flag' + ) + logger.error(e) + process.exitCode = 1 + return + } + logger.log(md) +} + +function mdTable< + T extends SocketSdkReturnType<'getAuditLogEvents'>['data']['results'] +>( + logs: T, + // 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: Array +): string { + // Max col width required to fit all data in that column + const cws = cols.map(col => col.length) + + for (const log of logs) { + for (let i = 0; i < cols.length; ++i) { + // @ts-ignore + const val: unknown = log[cols[i] ?? ''] ?? '' + cws[i] = Math.max(cws[i] ?? 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 log of logs) { + body += '|' + for (let i = 0; i < cols.length; ++i) { + // @ts-ignore + const val: unknown = log[cols[i] ?? ''] ?? '' + body += ' ' + String(val).padEnd(cws[i] ?? 0, ' ') + ' |' + } + body += '\n' + } + + return [div, header, div, body.trim(), div].filter(s => !!s.trim()).join('\n') +} + +async function outputAsPrint( + auditLogs: SocketSdkReturnType<'getAuditLogEvents'>['data']['results'], + orgSlug: string, + logType: string +): Promise { const data: AuditChoices = [] const logDetails: { [key: string]: string } = {} - for (const d of result.data.results) { + for (const d of auditLogs) { const { created_at } = d if (created_at) { const name = `${new Date(created_at).toLocaleDateString('en-us', { year: 'numeric', month: 'numeric', day: 'numeric' })} - ${d.user_email} - ${d.type} - ${d.ip_address} - ${d.user_agent}` @@ -68,8 +204,8 @@ export async function getAuditLog({ logger.log( logDetails[ (await select({ - message: type - ? `\n Audit log for: ${orgSlug} with type: ${type}\n` + message: logType + ? `\n Audit log for: ${orgSlug} with type: ${logType}\n` : `\n Audit log for: ${orgSlug}\n`, choices: data, pageSize: 30 @@ -77,3 +213,46 @@ export async function getAuditLog({ ] ) } + +async function getAuditLogWithToken({ + apiToken, + logType, + orgSlug, + outputKind, + page, + perPage +}: { + apiToken: string + outputKind: 'json' | 'markdown' | 'print' + orgSlug: string + page: number + perPage: number + logType: string +}): Promise['data'] | void> { + // Lazily access constants.spinner. + const { spinner } = constants + + spinner.start(`Looking up audit log for ${orgSlug}`) + + const socketSdk = await setupSdk(apiToken) + const result = await handleApiCall( + socketSdk.getAuditLogEvents(orgSlug, { + outputJson: outputKind === 'json', // I'm not sure this is used at all + outputMarkdown: outputKind === 'markdown', // I'm not sure this is used at all + orgSlug, + type: logType, + page, + per_page: perPage + }), + `Looking up audit log for ${orgSlug}\n` + ) + + if (!result.success) { + handleUnsuccessfulApiResponse('getAuditLogEvents', result, spinner) + return + } + + spinner.stop() + + return result.data +} diff --git a/test/dry-run.test.ts b/test/dry-run.test.ts index 9daded3b1..de5c8e341 100644 --- a/test/dry-run.test.ts +++ b/test/dry-run.test.ts @@ -161,7 +161,7 @@ describe('dry-run on all commands', async () => { expect(stderr).toMatchInlineSnapshot(` "\\x1b[31m\\xd7\\x1b[39m \\x1b[41m\\x1b[37mInput error\\x1b[39m\\x1b[49m: Please provide the required fields: - - Org name as the first argument \\x1b[31m(missing!)\\x1b[39m" + - Org name as the first argument \\x1b[31m(missing!)\\x1b[39m" `) expect(code).toBe(2)