Skip to content

Commit c147299

Browse files
author
Pelle Wessman
committed
Add new view command for reports
1 parent 9ca0f16 commit c147299

File tree

5 files changed

+237
-26
lines changed

5 files changed

+237
-26
lines changed

lib/commands/info/index.js

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import { ErrorWithCause } from 'pony-cause'
77

88
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
99
import { AuthError, InputError } from '../../utils/errors.js'
10+
import { getSeveritySummary } from '../../utils/format-issues.js'
1011
import { printFlagList } from '../../utils/formatting.js'
11-
import { stringJoinWithSeparateFinalSeparator } from '../../utils/misc.js'
1212
import { setupSdk } from '../../utils/sdk.js'
1313

1414
const description = 'Look up info regarding a package'
@@ -87,7 +87,7 @@ const run = async (argv, importMeta, { parentName }) => {
8787

8888
const spinner = ora(`Looking up data for version ${pkgVersion} of ${pkgName}`).start()
8989

90-
/** @type {Awaited<ReturnType<import('@socketsecurity/sdk').SocketSdk["getIssuesByNPMPackage"]>>} */
90+
/** @type {Awaited<ReturnType<typeof socketSdk.getIssuesByNPMPackage>>} */
9191
let result
9292

9393
try {
@@ -106,34 +106,12 @@ const run = async (argv, importMeta, { parentName }) => {
106106
process.exit(1)
107107
}
108108

109-
const data = result.data
110-
111-
/** @typedef {(typeof data)[number]["value"] extends infer U | undefined ? U : never} SocketSdkIssue */
112-
/** @type {Record<SocketSdkIssue["severity"], number>} */
113-
const severityCount = { low: 0, middle: 0, high: 0, critical: 0 }
114-
for (const issue of data) {
115-
const value = issue.value
116-
117-
if (!value) {
118-
continue
119-
}
120-
121-
if (severityCount[value.severity] !== undefined) {
122-
severityCount[value.severity] += 1
123-
}
124-
}
125-
126-
const issueSummary = stringJoinWithSeparateFinalSeparator([
127-
severityCount.critical ? severityCount.critical + ' critical' : undefined,
128-
severityCount.high ? severityCount.high + ' high' : undefined,
129-
severityCount.middle ? severityCount.middle + ' middle' : undefined,
130-
severityCount.low ? severityCount.low + ' low' : undefined,
131-
])
109+
const issueSummary = getSeveritySummary(result.data)
132110

133111
spinner.succeed(`Found ${issueSummary || 'no'} issues for version ${pkgVersion} of ${pkgName}`)
134112

135113
if (outputJson) {
136-
console.log(JSON.stringify(data, undefined, 2))
114+
console.log(JSON.stringify(result.data, undefined, 2))
137115
return
138116
}
139117

lib/commands/report/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { meowWithSubcommands } from '../../utils/meow-with-subcommands.js'
22
import { create } from './create.js'
3+
import { view } from './view.js'
34

45
const description = 'Project report related commands'
56

@@ -10,6 +11,7 @@ export const report = {
1011
await meowWithSubcommands(
1112
{
1213
create,
14+
view,
1315
},
1416
{
1517
argv,

lib/commands/report/view.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
/* eslint-disable no-console */
2+
3+
import chalk from 'chalk'
4+
import meow from 'meow'
5+
import ora from 'ora'
6+
7+
import { handleApiCall, handleUnsuccessfulApiResponse } from '../../utils/api-helpers.js'
8+
import { ChalkOrMarkdown } from '../../utils/chalk-markdown.js'
9+
import { InputError } from '../../utils/errors.js'
10+
import { getSeveritySummary } from '../../utils/format-issues.js'
11+
import { printFlagList } from '../../utils/formatting.js'
12+
import { setupSdk } from '../../utils/sdk.js'
13+
14+
/** @type {import('../../utils/meow-with-subcommands').CliSubcommand} */
15+
export const view = {
16+
description: 'View a project report',
17+
async run (argv, importMeta, { parentName }) {
18+
const name = parentName + ' view'
19+
20+
const input = setupCommand(name, view.description, argv, importMeta)
21+
const result = input && await fetchReportData(input.reportId)
22+
23+
if (result) {
24+
formatReportDataOutput(result.data, { name, ...input })
25+
}
26+
}
27+
}
28+
29+
// Internal functions
30+
31+
/**
32+
* @param {string} name
33+
* @param {string} description
34+
* @param {readonly string[]} argv
35+
* @param {ImportMeta} importMeta
36+
* @returns {void|{ outputJson: boolean, outputMarkdown: boolean, reportId: string }}
37+
*/
38+
function setupCommand (name, description, argv, importMeta) {
39+
// FIXME: Add examples
40+
const cli = meow(`
41+
Usage
42+
$ ${name} <report-identifier>
43+
44+
Options
45+
${printFlagList({
46+
'--json': 'Output result as json',
47+
'--markdown': 'Output result as markdown',
48+
}, 6)}
49+
`, {
50+
argv,
51+
description,
52+
importMeta,
53+
flags: {
54+
debug: {
55+
type: 'boolean',
56+
alias: 'd',
57+
default: false,
58+
},
59+
json: {
60+
type: 'boolean',
61+
alias: 'j',
62+
default: false,
63+
},
64+
markdown: {
65+
type: 'boolean',
66+
alias: 'm',
67+
default: false,
68+
},
69+
}
70+
})
71+
72+
// Extract the input
73+
74+
const {
75+
json: outputJson,
76+
markdown: outputMarkdown,
77+
} = cli.flags
78+
79+
const [reportId, ...extraInput] = cli.input
80+
81+
if (!reportId) {
82+
cli.showHelp()
83+
return
84+
}
85+
86+
// Validate the input
87+
88+
if (extraInput.length) {
89+
throw new InputError(`Can only handle a single report ID at a time, but got ${cli.input.length} report ID:s: ${cli.input.join(', ')}`)
90+
}
91+
92+
return {
93+
outputJson,
94+
outputMarkdown,
95+
reportId,
96+
}
97+
}
98+
99+
/**
100+
* @param {string} reportId
101+
* @returns {Promise<void|import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>>}
102+
*/
103+
async function fetchReportData (reportId) {
104+
// Do the API call
105+
106+
const socketSdk = await setupSdk()
107+
const spinner = ora(`Fetching report with ID ${reportId}`).start()
108+
const result = await handleApiCall(socketSdk.getReport(reportId), spinner, 'fetching report')
109+
110+
if (result.success === false) {
111+
return handleUnsuccessfulApiResponse(result, spinner)
112+
}
113+
114+
// Conclude the status of the API call
115+
116+
const issueSummary = getSeveritySummary(result.data.issues)
117+
spinner.succeed(`Report contains ${issueSummary || 'no'} issues`)
118+
119+
return result
120+
}
121+
122+
/**
123+
* @param {import('@socketsecurity/sdk').SocketSdkReturnType<'getReport'>["data"]} data
124+
* @param {{ name: string, outputJson: boolean, outputMarkdown: boolean, reportId: string }} context
125+
* @returns {void}
126+
*/
127+
function formatReportDataOutput (data, { name, outputJson, outputMarkdown, reportId }) {
128+
// If JSON, output and return...
129+
130+
if (outputJson) {
131+
console.log(JSON.stringify(data, undefined, 2))
132+
return
133+
}
134+
135+
// ...else do the CLI / Markdown output dance
136+
137+
const format = new ChalkOrMarkdown(!!outputMarkdown)
138+
const url = `https://socket.dev/npm/reports/${encodeURIComponent(reportId)}`
139+
140+
console.log('\nDetailed info on socket.dev: ' + format.hyperlink(reportId, url, { fallbackToUrl: true }))
141+
if (!outputMarkdown) {
142+
console.log(chalk.dim('\nOr rerun', chalk.italic(name), 'using the', chalk.italic('--json'), 'flag to get full JSON output'))
143+
}
144+
}

lib/utils/api-helpers.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import chalk from 'chalk'
2+
import { ErrorWithCause } from 'pony-cause'
3+
4+
import { AuthError } from './errors.js'
5+
6+
/**
7+
* @template T
8+
* @param {import('@socketsecurity/sdk').SocketSdkErrorType<T>} result
9+
* @param {import('ora').Ora} spinner
10+
* @returns {void}
11+
*/
12+
export function handleUnsuccessfulApiResponse (result, spinner) {
13+
const resultError = 'error' in result && result.error && typeof result.error === 'object' ? result.error : {}
14+
const message = 'message' in resultError && typeof resultError.message === 'string' ? resultError.message : 'No error message returned'
15+
16+
if (result.status === 401 || result.status === 403) {
17+
spinner.stop()
18+
throw new AuthError(message)
19+
}
20+
spinner.fail(chalk.white.bgRed('API returned an error:') + ' ' + message)
21+
process.exit(1)
22+
}
23+
24+
/**
25+
* @template T
26+
* @param {Promise<T>} value
27+
* @param {import('ora').Ora} spinner
28+
* @param {string} description
29+
* @returns {Promise<T>}
30+
*/
31+
export async function handleApiCall (value, spinner, description) {
32+
/** @type {T} */
33+
let result
34+
35+
try {
36+
result = await value
37+
} catch (cause) {
38+
spinner.fail()
39+
throw new ErrorWithCause(`Failed ${description}`, { cause })
40+
}
41+
42+
return result
43+
}

lib/utils/format-issues.js

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/** @typedef {import('@socketsecurity/sdk').SocketSdkReturnType<'getIssuesByNPMPackage'>['data']} SocketIssueList */
2+
/** @typedef {SocketIssueList[number]['value'] extends infer U | undefined ? U : never} SocketIssue */
3+
4+
import { stringJoinWithSeparateFinalSeparator } from './misc.js'
5+
6+
/**
7+
* @param {SocketIssueList} issues
8+
* @returns {Record<SocketIssue['severity'], number>}
9+
*/
10+
function getSeverityCount (issues) {
11+
/** @type {Record<SocketIssue['severity'], number>} */
12+
const severityCount = { low: 0, middle: 0, high: 0, critical: 0 }
13+
14+
for (const issue of issues) {
15+
const value = issue.value
16+
17+
if (!value) {
18+
continue
19+
}
20+
21+
if (severityCount[value.severity] !== undefined) {
22+
severityCount[value.severity] += 1
23+
}
24+
}
25+
26+
return severityCount
27+
}
28+
29+
/**
30+
* @param {SocketIssueList} issues
31+
* @returns {string}
32+
*/
33+
export function getSeveritySummary (issues) {
34+
const severityCount = getSeverityCount(issues)
35+
36+
const issueSummary = stringJoinWithSeparateFinalSeparator([
37+
severityCount.critical ? severityCount.critical + ' critical' : undefined,
38+
severityCount.high ? severityCount.high + ' high' : undefined,
39+
severityCount.middle ? severityCount.middle + ' middle' : undefined,
40+
severityCount.low ? severityCount.low + ' low' : undefined,
41+
])
42+
43+
return issueSummary
44+
}

0 commit comments

Comments
 (0)