Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
31 changes: 13 additions & 18 deletions src/commands/audit-log/cmd-audit-log.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -70,16 +68,22 @@ 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) {
// Use exit status of 2 to indicate incorrect usage, generally invalid
// 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
}

Expand All @@ -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)
})
}
241 changes: 210 additions & 31 deletions src/commands/audit-log/get-audit-log.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -12,51 +14,185 @@ type AuditChoice = Choice<string>
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<void> {
// 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<void> {
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<void> {
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 & keyof T[number]>
): 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<void> {
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}`
Expand All @@ -68,12 +204,55 @@ 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
})) as any
]
)
}

async function getAuditLogWithToken({
apiToken,
logType,
orgSlug,
outputKind,
page,
perPage
}: {
apiToken: string
outputKind: 'json' | 'markdown' | 'print'
orgSlug: string
page: number
perPage: number
logType: string
}): Promise<SocketSdkReturnType<'getAuditLogEvents'>['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
}
2 changes: 1 addition & 1 deletion test/dry-run.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down