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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Fixed
- Reachability scans in pnpm and yarn projects no longer print a 403 from the Socket API when run with a repository-scoped token. The safety check that runs before Coana launches now uses Socket's public token and only looks up the Coana package itself.

## [1.8.1](https://github.com/SocketDev/socket-cli/releases/tag/v1.8.1) - 2026-10-12

### Changed
Expand Down
76 changes: 69 additions & 7 deletions src/shadow/common.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,11 @@ import { logAlertsMap } from '../utils/socket-package-alert.mts'
import type { AlertsByPurl } from '../utils/socket-package-alert.mts'
import type { Spinner } from '@socketsecurity/registry/lib/spinner'

/**
* Extract package PURLs from command arguments for add/dlx commands where
* packages are specified as arguments.
* Used by: pnpm, yarn.
*/
function extractPackagePurlsFromCommandArgs(
rawArgs: string[] | readonly string[],
): string[] {
const packagePurls: string[] = []

// For 'add package1 package2@version' or 'dlx package', get packages from args.
const packageArgs = rawArgs
.slice(1)
.filter(a => !a.startsWith('-') && a !== '--')
Expand All @@ -39,6 +33,67 @@ function extractPackagePurlsFromCommandArgs(
return packagePurls
}

export type DlxFlags = {
// Options whose value names a package to download.
package: ReadonlySet<string>
// Other options that take the next argument as their value.
withValue: ReadonlySet<string>
}

/**
* Package option values, or else the first positional, name packages. Later
* positionals are arguments for the launched binary.
*/
function extractPackagePurlsFromDlxArgs(
rawArgs: string[] | readonly string[],
dlxFlags: DlxFlags,
): string[] {
const pkgSpecs: string[] = []
const args = rawArgs.slice(1)
for (let i = 0, { length } = args; i < length; i += 1) {
const arg = args[i]!
if (arg === '--') {
break
}
if (arg.startsWith('-')) {
const eqIndex = arg.indexOf('=')
const name = eqIndex === -1 ? arg : arg.slice(0, eqIndex)
const isPackageFlag = dlxFlags.package.has(name)
if (eqIndex !== -1) {
if (isPackageFlag) {
pkgSpecs.push(arg.slice(eqIndex + 1))
}
continue
}
const value = args[i + 1]
if (
(isPackageFlag || dlxFlags.withValue.has(name)) &&
value !== undefined &&
!value.startsWith('-')
) {
if (isPackageFlag) {
pkgSpecs.push(value)
}
i += 1
}
continue
}
if (!pkgSpecs.length) {
pkgSpecs.push(arg)
}
break
}

const packagePurls: string[] = []
for (const pkgSpec of pkgSpecs) {
const purl = safeNpmSpecToPurl(pkgSpec)
if (purl) {
packagePurls.push(purl)
}
}
return packagePurls
}

/**
* Extract package PURLs from package.json for install/update commands.
* Used by: pnpm, yarn.
Expand Down Expand Up @@ -81,9 +136,11 @@ async function extractPackagePurlsFromPackageJson(

export type PackageScanOptions = {
acceptRisks: boolean
apiToken?: string | undefined
command: string | undefined
cwd?: string | URL
dlxCommands?: Set<string>
dlxFlags?: DlxFlags | undefined
installCommands: Set<string>
managerName: string
nothrow?: boolean
Expand All @@ -105,8 +162,10 @@ export async function scanPackagesAndLogAlerts(
): Promise<PackageScanResult> {
const {
acceptRisks,
apiToken,
command,
dlxCommands,
dlxFlags = { package: new Set(), withValue: new Set() },
installCommands,
managerName,
nothrow = true,
Expand All @@ -132,7 +191,9 @@ export async function scanPackagesAndLogAlerts(
// Extract package names from command arguments before any downloads.
let packagePurls: string[] = []

if (isDlxCommand || isAddCommand(command)) {
if (isDlxCommand) {
packagePurls = extractPackagePurlsFromDlxArgs(rawArgs, dlxFlags)
} else if (isAddCommand(command)) {
packagePurls = extractPackagePurlsFromCommandArgs(rawArgs)
} else if (isInstallCommand) {
// For install/update, scan dependencies from package.json.
Expand All @@ -149,6 +210,7 @@ export async function scanPackagesAndLogAlerts(

try {
const alertsMap = await getAlertsMapFromPurls(packagePurls, {
apiToken,
filter: acceptRisks
? { actions: ['error'], blocked: true }
: { actions: ['error', 'monitor', 'warn'] },
Expand Down
33 changes: 32 additions & 1 deletion src/shadow/pnpm/bin.mts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { scanPackagesAndLogAlerts } from '../common.mts'
import { ensureIpcInStdio } from '../stdio-ipc.mts'

import type { IpcObject } from '../../constants.mts'
import type { DlxFlags } from '../common.mts'
import type {
SpawnExtra,
SpawnOptions,
Expand All @@ -41,6 +42,30 @@ export type ShadowPnpmResult = {

const DLX_COMMANDS = new Set(['dlx'])

// pnpm 10's dlx and global options that take a value. pnpm stops parsing
// options at the first positional after `dlx`.
const DLX_FLAGS: DlxFlags = {
package: new Set(['--package']),
withValue: new Set([
'--allow-build',
'--changed-files-ignore-pattern',
'--cpu',
'--dir',
'--filter',
'--filter-prod',
'--libc',
'--loglevel',
'--os',
'--prefix',
'--reporter',
'--test-pattern',
'--use-node-version',
'--workspace-packages',
'-C',
'-F',
]),
}

const INSTALL_COMMANDS = new Set([
'add',
'i',
Expand Down Expand Up @@ -79,16 +104,21 @@ export default async function shadowPnpmBin(
spinner?.start()

if (needsScanning && !rawPnpmArgs.includes(FLAG_DRY_RUN)) {
const acceptRisks = !!constants.ENV.SOCKET_CLI_ACCEPT_RISKS
const acceptRisks =
!!ipc?.[constants.SOCKET_CLI_SHADOW_ACCEPT_RISKS] ||
!!constants.ENV.SOCKET_CLI_ACCEPT_RISKS
const apiToken = ipc?.[constants.SOCKET_CLI_SHADOW_API_TOKEN]
const viewAllRisks = !!constants.ENV.SOCKET_CLI_VIEW_ALL_RISKS

// Handle add and dlx commands with shared utility.
if (isDlxCommand || isAddCommand(command)) {
const scanResult = await scanPackagesAndLogAlerts({
acceptRisks,
apiToken,
command,
cwd,
dlxCommands: DLX_COMMANDS,
dlxFlags: DLX_FLAGS,
installCommands: INSTALL_COMMANDS,
managerName: PNPM,
rawArgs: rawPnpmArgs,
Expand Down Expand Up @@ -118,6 +148,7 @@ export default async function shadowPnpmBin(
)

const alertsMap = await getAlertsMapFromPnpmLockfile(lockfile, {
apiToken,
nothrow: true,
filter: acceptRisks
? { actions: ['error'], blocked: true }
Expand Down
105 changes: 105 additions & 0 deletions src/shadow/pnpm/bin.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,109 @@ describe('shadowPnpm', () => {
}),
)
})

it('should scan only the package spec for dlx, not the binary arguments', async () => {
await shadowPnpm([
'dlx',
'@coana-tech/cli@15.10.55',
'run',
'.',
'--output-dir',
'/tmp/out',
'--analysis-timeout',
'3h',
])

expect(mockGetAlertsMapFromPurls).toHaveBeenCalledWith(
['pkg:npm/@coana-tech/cli@15.10.55'],
expect.objectContaining({
nothrow: true,
}),
)
})

it('should scan --package values for dlx', async () => {
await shadowPnpm([
'dlx',
'--package',
'cowsay@1.6.0',
'--package=lodash@4.17.21',
'cowsay',
'hello',
])

expect(mockGetAlertsMapFromPurls).toHaveBeenCalledWith(
['pkg:npm/cowsay@1.6.0', 'pkg:npm/lodash@4.17.21'],
expect.objectContaining({
nothrow: true,
}),
)
})

it('should skip dlx option values when picking the package', async () => {
await shadowPnpm([
'dlx',
'--allow-build',
'esbuild',
'--reporter',
'append-only',
'vite@6.0.0',
'--help',
])

expect(mockGetAlertsMapFromPurls).toHaveBeenCalledWith(
['pkg:npm/vite@6.0.0'],
expect.objectContaining({
nothrow: true,
}),
)
})

it('should scan every --package across other dlx options', async () => {
await shadowPnpm([
'dlx',
'--package',
'cowsay@1.6.0',
'--allow-build',
'esbuild',
'--package',
'lodash@4.17.21',
'cowsay',
])

expect(mockGetAlertsMapFromPurls).toHaveBeenCalledWith(
['pkg:npm/cowsay@1.6.0', 'pkg:npm/lodash@4.17.21'],
expect.objectContaining({
nothrow: true,
}),
)
})

it('should treat -p as a boolean option for dlx', async () => {
await shadowPnpm(['dlx', '-p', 'cowsay@1.6.0', 'hello'])

expect(mockGetAlertsMapFromPurls).toHaveBeenCalledWith(
['pkg:npm/cowsay@1.6.0'],
expect.objectContaining({
nothrow: true,
}),
)
})

it('should use the IPC API token and accept-risks for dlx scans', async () => {
await shadowPnpm(['dlx', 'cowsay@1.6.0'], {
ipc: {
SOCKET_CLI_SHADOW_ACCEPT_RISKS: true,
SOCKET_CLI_SHADOW_API_TOKEN: 'public-token',
},
})

expect(mockGetAlertsMapFromPurls).toHaveBeenCalledWith(
['pkg:npm/cowsay@1.6.0'],
expect.objectContaining({
apiToken: 'public-token',
filter: { actions: ['error'], blocked: true },
}),
)
})
})
12 changes: 11 additions & 1 deletion src/shadow/yarn/bin.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { scanPackagesAndLogAlerts } from '../common.mts'
import { ensureIpcInStdio } from '../stdio-ipc.mts'

import type { IpcObject } from '../../constants.mts'
import type { DlxFlags } from '../common.mts'
import type {
SpawnExtra,
SpawnOptions,
Expand All @@ -27,6 +28,11 @@ export type ShadowYarnResult = {

const DLX_COMMANDS = new Set(['dlx'])

const DLX_FLAGS: DlxFlags = {
package: new Set(['--package', '-p']),
withValue: new Set(),
}

const INSTALL_COMMANDS = new Set([
'add',
'install',
Expand Down Expand Up @@ -59,10 +65,14 @@ export default async function shadowYarnBin(
// Check for package scanning.
const command = rawYarnArgs[0]
const scanResult = await scanPackagesAndLogAlerts({
acceptRisks: !!constants.ENV.SOCKET_CLI_ACCEPT_RISKS,
acceptRisks:
!!ipc?.[constants.SOCKET_CLI_SHADOW_ACCEPT_RISKS] ||
!!constants.ENV.SOCKET_CLI_ACCEPT_RISKS,
apiToken: ipc?.[constants.SOCKET_CLI_SHADOW_API_TOKEN],
command,
cwd,
dlxCommands: DLX_COMMANDS,
dlxFlags: DLX_FLAGS,
installCommands: INSTALL_COMMANDS,
managerName: YARN,
rawArgs: rawYarnArgs,
Expand Down
Loading
Loading