Skip to content

Commit 22ffec6

Browse files
committed
Add the package score command, for realzies
1 parent 581b04a commit 22ffec6

File tree

6 files changed

+397
-2
lines changed

6 files changed

+397
-2
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import path from 'node:path'
2+
3+
import { describe, expect } from 'vitest'
4+
5+
import constants from '../../../dist/constants.js'
6+
import { cmdit, invokeNpm } from '../../../test/utils'
7+
8+
const { CLI } = constants
9+
10+
describe('socket package score', async () => {
11+
// Lazily access constants.rootBinPath.
12+
const entryPath = path.join(constants.rootBinPath, `${CLI}.js`)
13+
14+
cmdit(['package', 'score', '--help'], 'should support --help', async cmd => {
15+
const { code, stderr, stdout } = await invokeNpm(entryPath, cmd)
16+
expect(stdout).toMatchInlineSnapshot(
17+
`
18+
"Look up score for one package which reflects all of its transitive dependencies as well
19+
20+
Usage
21+
$ socket package score <<ecosystem> <name> | <purl>>
22+
23+
Options
24+
--dryRun Do input validation for a command and exit 0 when input is ok
25+
--help Print this help.
26+
--json Output result as json
27+
--markdown Output result as markdown
28+
29+
Requirements
30+
- quota: 100
31+
- scope: \`packages:list\`
32+
33+
Show deep scoring details for one package. The score will reflect the package
34+
itself, any of its dependencies, and any of its transitive dependencies.
35+
36+
When you want to know whether to trust a package, this is the command to run.
37+
38+
See also the \`socket package shallow\` command, which returns the shallow
39+
score for any number of packages. That will not reflect the dependency scores.
40+
41+
Only a few ecosystems are supported like npm, golang, and maven.
42+
43+
A "purl" is a standard package name formatting: \`pkg:eco/name@version\`
44+
This command will automatically prepend "pkg:" when not present.
45+
46+
The version is optional but when given should be a direct match.
47+
48+
Examples
49+
$ socket package score npm babel-cli
50+
$ socket package score npm babel-cli@1.9.1
51+
$ socket package score npm/babel-cli@1.9.1
52+
$ socket package score pkg:npm/babel-cli@1.9.1"
53+
`
54+
)
55+
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
56+
"
57+
_____ _ _ /---------------
58+
| __|___ ___| |_ ___| |_ | Socket.dev CLI ver <redacted>
59+
|__ | . | _| '_| -_| _| | Node: <redacted>, API token set: <redacted>
60+
|_____|___|___|_,_|___|_|.dev | Command: \`socket package score\`, cwd: <redacted>"
61+
`)
62+
63+
expect(code, 'help should exit with code 2').toBe(2)
64+
expect(stderr, 'header should include command (without params)').toContain(
65+
'`socket package score`'
66+
)
67+
})
68+
69+
cmdit(
70+
['package', 'score', '--dry-run'],
71+
'should require args with just dry-run',
72+
async cmd => {
73+
const { code, stderr, stdout } = await invokeNpm(entryPath, cmd)
74+
expect(stdout).toMatchInlineSnapshot(`""`)
75+
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
76+
"
77+
_____ _ _ /---------------
78+
| __|___ ___| |_ ___| |_ | Socket.dev CLI ver <redacted>
79+
|__ | . | _| '_| -_| _| | Node: <redacted>, API token set: <redacted>
80+
|_____|___|___|_,_|___|_|.dev | Command: \`socket package score\`, cwd: <redacted>
81+
82+
\\x1b[31m\\xd7\\x1b[39m \\x1b[41m\\x1b[37mInput error\\x1b[39m\\x1b[49m: Please provide the required fields:
83+
84+
- First parameter should be an ecosystem or the arg must be a purl \\x1b[31m(bad!)\\x1b[39m
85+
86+
- Expecting the package to check \\x1b[31m(missing!)\\x1b[39m"
87+
`)
88+
89+
expect(code, 'dry-run should exit with code 2 if missing input').toBe(2)
90+
}
91+
)
92+
93+
cmdit(
94+
['package', 'score', 'npm', 'babel', '--dry-run'],
95+
'should require args with just dry-run',
96+
async cmd => {
97+
const { code, stderr, stdout } = await invokeNpm(entryPath, cmd)
98+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
99+
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
100+
"
101+
_____ _ _ /---------------
102+
| __|___ ___| |_ ___| |_ | Socket.dev CLI ver <redacted>
103+
|__ | . | _| '_| -_| _| | Node: <redacted>, API token set: <redacted>
104+
|_____|___|___|_,_|___|_|.dev | Command: \`socket package score\`, cwd: <redacted>"
105+
`)
106+
107+
expect(code, 'dry-run should exit with code 0 if input ok').toBe(0)
108+
}
109+
)
110+
})
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import colors from 'yoctocolors-cjs'
2+
3+
import { logger } from '@socketsecurity/registry/lib/logger'
4+
5+
import { handlePurlDeepScore } from './handle-purl-deep-score'
6+
import { parsePackageSpecifiers } from './parse-package-specifiers'
7+
import constants from '../../constants'
8+
import { commonFlags, outputFlags } from '../../flags'
9+
import { meowOrExit } from '../../utils/meow-with-subcommands'
10+
import { getFlagListOutput } from '../../utils/output-formatting'
11+
12+
import type { CliCommandConfig } from '../../utils/meow-with-subcommands'
13+
14+
const { DRY_RUN_BAIL_TEXT } = constants
15+
16+
const config: CliCommandConfig = {
17+
commandName: 'score',
18+
description:
19+
'Look up score for one package which reflects all of its transitive dependencies as well',
20+
hidden: true,
21+
flags: {
22+
...commonFlags,
23+
...outputFlags
24+
},
25+
help: (command, config) => `
26+
Usage
27+
$ ${command} <<ecosystem> <name> | <purl>>
28+
29+
Options
30+
${getFlagListOutput(config.flags, 6)}
31+
32+
Requirements
33+
- quota: 100
34+
- scope: \`packages:list\`
35+
36+
Show deep scoring details for one package. The score will reflect the package
37+
itself, any of its dependencies, and any of its transitive dependencies.
38+
39+
When you want to know whether to trust a package, this is the command to run.
40+
41+
See also the \`socket package shallow\` command, which returns the shallow
42+
score for any number of packages. That will not reflect the dependency scores.
43+
44+
Only a few ecosystems are supported like npm, golang, and maven.
45+
46+
A "purl" is a standard package name formatting: \`pkg:eco/name@version\`
47+
This command will automatically prepend "pkg:" when not present.
48+
49+
The version is optional but when given should be a direct match.
50+
51+
Examples
52+
$ ${command} npm babel-cli
53+
$ ${command} npm babel-cli@1.9.1
54+
$ ${command} npm/babel-cli@1.9.1
55+
$ ${command} pkg:npm/babel-cli@1.9.1
56+
`
57+
}
58+
59+
export const cmdPackageScore = {
60+
description: config.description,
61+
hidden: config.hidden,
62+
run
63+
}
64+
65+
async function run(
66+
argv: string[] | readonly string[],
67+
importMeta: ImportMeta,
68+
{ parentName }: { parentName: string }
69+
): Promise<void> {
70+
const cli = meowOrExit({
71+
argv,
72+
config,
73+
importMeta,
74+
parentName
75+
})
76+
77+
const { json, markdown } = cli.flags
78+
const [ecosystem = '', purl] = cli.input
79+
80+
const { purls, valid } = parsePackageSpecifiers(ecosystem, purl ? [purl] : [])
81+
82+
if (!valid || !purls.length) {
83+
// Use exit status of 2 to indicate incorrect usage, generally invalid
84+
// options or missing arguments.
85+
// https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html
86+
process.exitCode = 2
87+
logger.fail(`${colors.bgRed(colors.white('Input error'))}: Please provide the required fields:\n
88+
- First parameter should be an ecosystem or the arg must be a purl ${!valid ? colors.red('(bad!)') : colors.green('(ok)')}\n
89+
- Expecting the package to check ${!purls.length ? colors.red('(missing!)') : colors.green('(ok)')}\n
90+
`)
91+
return
92+
}
93+
94+
if (cli.flags['dryRun']) {
95+
logger.log(DRY_RUN_BAIL_TEXT)
96+
return
97+
}
98+
99+
await handlePurlDeepScore(
100+
purls[0] || '',
101+
json ? 'json' : markdown ? 'markdown' : 'text'
102+
)
103+
}

src/commands/package/cmd-package.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { cmdPackageScore } from './cmd-package-score'
12
import { cmdPackageShallow } from './cmd-package-shallow'
23
import { meowWithSubcommands } from '../../utils/meow-with-subcommands'
34

@@ -11,14 +12,15 @@ export const cmdPackage: CliSubcommand = {
1112
async run(argv, importMeta, { parentName }) {
1213
await meowWithSubcommands(
1314
{
15+
score: cmdPackageScore,
1416
shallow: cmdPackageShallow
1517
},
1618
{
1719
aliases: {
18-
pkg: {
20+
deep: {
1921
description,
2022
hidden: true,
21-
argv: []
23+
argv: ['score']
2224
}
2325
},
2426
argv,
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import colors from 'yoctocolors-cjs'
2+
3+
import { logger } from '@socketsecurity/registry/lib/logger'
4+
5+
import constants from '../../constants'
6+
import { handleAPIError, handleApiCall, queryAPI } from '../../utils/api'
7+
import { AuthError } from '../../utils/errors'
8+
import { getDefaultToken } from '../../utils/sdk'
9+
10+
export async function fetchPurlDeepScore(purl: string) {
11+
// Lazily access constants.spinner.
12+
const { spinner } = constants
13+
14+
const apiToken = getDefaultToken()
15+
if (!apiToken) {
16+
throw new AuthError(
17+
'User must be authenticated to run this command. To log in, run the command `socket login` and enter your API key.'
18+
)
19+
}
20+
21+
spinner.start('Getting deep package score...')
22+
const response = await queryAPI(
23+
`purl/score/${encodeURIComponent(purl)}`,
24+
apiToken
25+
)
26+
spinner?.successAndStop('Received deep package score response.')
27+
28+
if (!response.ok) {
29+
const err = await handleAPIError(response.status)
30+
logger.log('\nThere was an error', err)
31+
spinner.errorAndStop(
32+
`${colors.bgRed(colors.white(response.statusText))}: ${err}`
33+
)
34+
return
35+
}
36+
37+
const result = await handleApiCall(await response.text(), 'Reading text')
38+
39+
try {
40+
return JSON.parse(result)
41+
} catch (e) {
42+
throw new Error(
43+
'Was unable to JSON parse the input from the server. It may not have been a proper JSON response. Please report this problem.'
44+
)
45+
}
46+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { fetchPurlDeepScore } from './fetch-purl-deep-score'
2+
import { outputPurlScore } from './output-purl-score'
3+
4+
export async function handlePurlDeepScore(
5+
purl: string,
6+
outputKind: 'json' | 'markdown' | 'text'
7+
) {
8+
const data = await fetchPurlDeepScore(purl)
9+
if (!data) return
10+
11+
await outputPurlScore(purl, data, outputKind)
12+
}

0 commit comments

Comments
 (0)