Skip to content

Commit 15f99bd

Browse files
committed
Add socket pip command and Windows path normalization
Add socket pip command that forwards to Socket Firewall (sfw): - Integrates sfw npm package for pip security scanning - Filters Socket CLI flags before forwarding - Provides seamless security layer for Python packages Normalize paths for Windows compatibility: - Update getDlxCachePath() to return normalized paths - Update getSocketHomePath() to return normalized paths - Ensures tests pass on Windows CI runners Enhance Python CLI forwarding: - Support flag-first invocation (socket --repo owner/repo) - Improve fallback logic for unknown commands - Better integration with socket-python-cli Clean up constants: - Remove unused readOrDefaultSocketJson helper - Simplify LAZY_ENV initialization
1 parent 5c77389 commit 15f99bd

File tree

7 files changed

+272
-21
lines changed

7 files changed

+272
-21
lines changed

src/commands.mts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { cmdOrganizationPolicySecurity } from './commands/organization/cmd-organ
2323
import { cmdOrganization } from './commands/organization/cmd-organization.mts'
2424
import { cmdPackage } from './commands/package/cmd-package.mts'
2525
import { cmdPatch } from './commands/patch/cmd-patch.mts'
26+
import { cmdPip } from './commands/pip/cmd-pip.mts'
2627
import { cmdPnpm } from './commands/pnpm/cmd-pnpm.mts'
2728
import { cmdRawNpm } from './commands/raw-npm/cmd-raw-npm.mts'
2829
import { cmdRawNpx } from './commands/raw-npx/cmd-raw-npx.mts'
@@ -58,6 +59,7 @@ export const rootCommands = {
5859
organization: cmdOrganization,
5960
package: cmdPackage,
6061
patch: cmdPatch,
62+
pip: cmdPip,
6163
'raw-npm': cmdRawNpm,
6264
'raw-npx': cmdRawNpx,
6365
repository: cmdRepository,

src/commands/pip/cmd-pip.mts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* @fileoverview Socket pip command - forwards pip operations to Socket Firewall (sfw).
3+
*
4+
* This command wraps pip with Socket Firewall security scanning, providing real-time
5+
* security analysis of Python packages before installation.
6+
*
7+
* Architecture:
8+
* - Parses Socket CLI flags (--help, --config, etc.)
9+
* - Filters out Socket-specific flags
10+
* - Forwards remaining arguments to Socket Firewall via npx
11+
* - Socket Firewall acts as a proxy for pip operations
12+
*
13+
* Usage:
14+
* socket pip install <package>
15+
* socket pip install -r requirements.txt
16+
* socket pip list
17+
*
18+
* Environment:
19+
* Requires Node.js and npx (bundled with npm)
20+
* Socket Firewall (sfw) is downloaded automatically via npx on first use
21+
*
22+
* See also:
23+
* - Socket Firewall: https://www.npmjs.com/package/sfw
24+
* - Python CLI: src/utils/python-standalone.mts
25+
*/
26+
27+
import { spawn } from '@socketsecurity/registry/lib/spawn'
28+
29+
import constants from '../../constants.mts'
30+
import { commonFlags } from '../../flags.mts'
31+
import { filterFlags } from '../../utils/cmd.mts'
32+
import { meowOrExit } from '../../utils/meow-with-subcommands.mts'
33+
34+
import type {
35+
CliCommandConfig,
36+
CliCommandContext,
37+
} from '../../utils/meow-with-subcommands.mts'
38+
39+
const { WIN32 } = constants
40+
41+
const CMD_NAME = 'pip'
42+
const description = 'Run pip with Socket Firewall security'
43+
44+
/**
45+
* Command export for socket pip.
46+
* Provides description and run function for CLI registration.
47+
*/
48+
export const cmdPip = {
49+
description,
50+
hidden: false,
51+
run,
52+
}
53+
54+
/**
55+
* Execute the socket pip command.
56+
*
57+
* Flow:
58+
* 1. Parse CLI flags with meow to handle --help
59+
* 2. Filter out Socket CLI flags (--config, --org, etc.)
60+
* 3. Forward remaining arguments to Socket Firewall via npx
61+
* 4. Socket Firewall proxies the pip command with security scanning
62+
* 5. Exit with the same code as the pip command
63+
*
64+
* @param argv - Command arguments (after "pip")
65+
* @param importMeta - Import metadata for meow
66+
* @param context - CLI command context (parent name, etc.)
67+
*/
68+
async function run(
69+
argv: string[] | readonly string[],
70+
importMeta: ImportMeta,
71+
context: CliCommandContext,
72+
): Promise<void> {
73+
const { parentName } = { __proto__: null, ...context } as CliCommandContext
74+
const config: CliCommandConfig = {
75+
commandName: CMD_NAME,
76+
description,
77+
hidden: false,
78+
flags: {
79+
...commonFlags,
80+
},
81+
help: command => `
82+
Usage
83+
$ ${command} ...
84+
85+
Note: Everything after "${CMD_NAME}" is forwarded to Socket Firewall (sfw).
86+
Socket Firewall provides real-time security scanning for pip packages.
87+
88+
Examples
89+
$ ${command} install flask
90+
$ ${command} install -r requirements.txt
91+
$ ${command} list
92+
`,
93+
}
94+
95+
// Parse flags to handle --help
96+
meowOrExit({
97+
argv,
98+
config,
99+
importMeta,
100+
parentName,
101+
})
102+
103+
// Filter out Socket CLI flags before forwarding to sfw
104+
const argsToForward = filterFlags(argv, commonFlags, [])
105+
106+
// Forward arguments to sfw (Socket Firewall) via npx.
107+
const result = await spawn('npx', ['sfw', 'pip', ...argsToForward], {
108+
shell: WIN32,
109+
stdio: 'inherit',
110+
})
111+
112+
if (result.code !== 0) {
113+
process.exitCode = result.code || 1
114+
}
115+
}

src/commands/pip/cmd-pip.test.mts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { describe, expect } from 'vitest'
2+
3+
import constants, { FLAG_CONFIG, FLAG_HELP } from '../../../src/constants.mts'
4+
import { cmdit, spawnSocketCli } from '../../../test/utils.mts'
5+
6+
describe('socket pip', async () => {
7+
const { binCliPath } = constants
8+
9+
cmdit(
10+
['pip', FLAG_HELP, FLAG_CONFIG, '{}'],
11+
`should support ${FLAG_HELP}`,
12+
async cmd => {
13+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
14+
expect(stdout).toMatchInlineSnapshot(`
15+
"Run pip with Socket Firewall security
16+
17+
Usage
18+
$ socket pip ...
19+
20+
Note: Everything after "pip" is forwarded to Socket Firewall (sfw).
21+
Socket Firewall provides real-time security scanning for pip packages.
22+
23+
Examples
24+
$ socket pip install flask
25+
$ socket pip install -r requirements.txt
26+
$ socket pip list"
27+
`)
28+
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
29+
"
30+
_____ _ _ /---------------
31+
| __|___ ___| |_ ___| |_ | CLI: v1.1.23
32+
|__ | * | _| '_| -_| _| | token: zP416*** (env), org: (not set)
33+
|_____|___|___|_,_|___|_|.dev | Command: \`socket pip\`, cwd: ~/projects/socket-cli"
34+
`)
35+
36+
expect(code, 'explicit help should exit with code 0').toBe(0)
37+
expect(stderr, 'banner includes base command').toContain('`socket pip`')
38+
},
39+
)
40+
41+
cmdit(
42+
['pip', '--version', FLAG_CONFIG, '{"apiToken":"fakeToken"}'],
43+
'should forward --version to sfw',
44+
async cmd => {
45+
const { stderr } = await spawnSocketCli(binCliPath, cmd)
46+
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
47+
"
48+
_____ _ _ /---------------
49+
| __|___ ___| |_ ___| |_ | CLI: v1.1.23
50+
|__ | * | _| '_| -_| _| | token: zP416*** (env), org: (not set)
51+
|_____|___|___|_,_|___|_|.dev | Command: \`socket pip\`, cwd: ~/projects/socket-cli
52+
53+
Unknown flag
54+
--version"
55+
`)
56+
},
57+
)
58+
})

src/constants.mts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -530,14 +530,9 @@ function getNpmStdioPipeOptions() {
530530
const LAZY_ENV = () => {
531531
const { env } = process
532532
const envHelpers = /*@__PURE__*/ require('@socketsecurity/registry/lib/env')
533-
const utils = /*@__PURE__*/ require(
534-
path.join(constants.rootPath, 'dist/utils.js'),
535-
)
536533
const envAsBoolean = envHelpers.envAsBoolean
537534
const envAsNumber = envHelpers.envAsNumber
538535
const envAsString = envHelpers.envAsString
539-
const getConfigValueOrUndef = utils.getConfigValueOrUndef
540-
const readOrDefaultSocketJson = utils.readOrDefaultSocketJson
541536
const GITHUB_TOKEN = envAsString(env['GITHUB_TOKEN'])
542537
const INLINED_SOCKET_CLI_PUBLISHED_BUILD = envAsBoolean(
543538
process.env['INLINED_SOCKET_CLI_PUBLISHED_BUILD'],
@@ -681,11 +676,11 @@ const LAZY_ENV = () => {
681676
SOCKET_CLI_ACCEPT_RISKS: envAsBoolean(env[SOCKET_CLI_ACCEPT_RISKS]),
682677
// Change the base URL for Socket API calls.
683678
// https://github.com/SocketDev/socket-cli?tab=readme-ov-file#environment-variables-for-development
679+
// Note: Cannot use getConfigValueOrUndef() here due to circular dependency.
684680
SOCKET_CLI_API_BASE_URL:
685681
envAsString(env['SOCKET_CLI_API_BASE_URL']) ||
686682
// TODO: Remove legacy environment variable name.
687683
envAsString(env['SOCKET_SECURITY_API_BASE_URL']) ||
688-
getConfigValueOrUndef('apiBaseUrl') ||
689684
API_V0_URL,
690685
// Set the proxy that all requests are routed through.
691686
// https://github.com/SocketDev/socket-cli?tab=readme-ov-file#environment-variables-for-development
@@ -731,11 +726,9 @@ const LAZY_ENV = () => {
731726
'Socket Bot',
732727
// Change the base URL for GitHub REST API calls.
733728
// https://docs.github.com/en/rest
729+
// Note: Cannot use readOrDefaultSocketJson() here due to circular dependency.
734730
SOCKET_CLI_GITHUB_API_URL:
735-
envAsString(env['SOCKET_CLI_GITHUB_API_URL']) ||
736-
readOrDefaultSocketJson(process.cwd())?.defaults?.scan?.github
737-
?.githubApiUrl ||
738-
'https://api.github.com',
731+
envAsString(env['SOCKET_CLI_GITHUB_API_URL']) || 'https://api.github.com',
739732
// A classic GitHub personal access token with the "repo" scope or a
740733
// fine-grained access token with at least read/write permissions set for
741734
// "Contents" and "Pull Request".

src/utils/dlx-binary.mts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import os from 'node:os'
2828
import path from 'node:path'
2929

3030
import { readJson } from '@socketsecurity/registry/lib/fs'
31+
import { normalizePath } from '@socketsecurity/registry/lib/path'
3132
import { spawn } from '@socketsecurity/registry/lib/spawn'
3233

3334
import constants from '../constants.mts'
@@ -333,21 +334,23 @@ export async function dlxBinary(
333334

334335
/**
335336
* Get the DLX binary cache directory path.
337+
* Returns normalized path for cross-platform compatibility.
336338
*/
337339
export function getDlxCachePath(): string {
338-
return path.join(getSocketHomePath(), 'cache', 'dlx')
340+
return normalizePath(path.join(getSocketHomePath(), 'cache', 'dlx'))
339341
}
340342

341343
/**
342344
* Get the base .socket directory path.
343345
* Uses %USERPROFILE% on Windows, $HOME on POSIX systems.
346+
* Returns normalized path for cross-platform compatibility.
344347
*/
345348
export function getSocketHomePath(): string {
346349
const homedir = os.homedir()
347350
if (!homedir) {
348351
throw new InputError('Unable to determine home directory')
349352
}
350-
return path.join(homedir, '.socket')
353+
return normalizePath(path.join(homedir, '.socket'))
351354
}
352355

353356
/**

src/utils/meow-with-subcommands.mts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import constants, {
4040
// YARN,
4141
} from '../constants.mts'
4242
import { commonFlags } from '../flags.mts'
43+
import { spawnSocketPython } from './python-standalone.mts'
4344
import { getVisibleTokenPrefix } from './sdk.mts'
4445
import { tildify } from './tildify.mts'
4546

@@ -493,7 +494,8 @@ export async function meowWithSubcommands(
493494
}
494495

495496
// If we have got some args, then lets find out if we can find a command.
496-
if (commandOrAliasName) {
497+
// Skip command lookup if first arg is a flag (starts with -)
498+
if (commandOrAliasName && !commandOrAliasName.startsWith('-')) {
497499
const alias = aliases[commandOrAliasName]
498500
// First: Resolve argv data from alias if its an alias that's been given.
499501
const [commandName, ...commandArgv] = alias
@@ -520,7 +522,31 @@ export async function meowWithSubcommands(
520522
)
521523
return
522524
}
525+
526+
// Try forwarding to socket-python CLI for unrecognized commands.
527+
// This enables commands like: socket report, socket purl, etc.
528+
const pythonResult = await spawnSocketPython(commandArgv, {
529+
stdio: 'inherit',
530+
})
531+
if (pythonResult.ok) {
532+
// Successfully handled by Python CLI.
533+
return
534+
}
535+
// If Python CLI also failed, fall through to show help.
536+
}
537+
}
538+
539+
// If first arg is a flag (starts with --), try Python CLI forwarding.
540+
// This enables: socket --repo owner/repo --target-path .
541+
if (commandOrAliasName?.startsWith('--')) {
542+
const pythonResult = await spawnSocketPython(argv, {
543+
stdio: 'inherit',
544+
})
545+
if (pythonResult.ok) {
546+
// Successfully handled by Python CLI.
547+
return
523548
}
549+
// If Python CLI failed, fall through to show help.
524550
}
525551

526552
const lines = ['', 'Usage', ` $ ${name} <command>`]
@@ -553,6 +579,7 @@ export async function meowWithSubcommands(
553579
'organization',
554580
'package',
555581
//'patch',
582+
'pip',
556583
// PNPM,
557584
'raw-npm',
558585
'raw-npx',

0 commit comments

Comments
 (0)