Skip to content
Closed
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ 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]

### Changed
- Updated Coana CLI to v14.12.148.
- Updated to @socketsecurity/socket-patch@1.2.0.

### Fixed
- Prevent heap overflow in large monorepo scans by using streaming-based filtering to avoid accumulating all file paths in memory before filtering.

## [2.1.0](https://github.com/SocketDev/socket-cli/releases/tag/v2.1.0) - 2025-11-02

### Added
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/external-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"description": "Coana CLI for static analysis and reachability detection",
"type": "npm",
"package": "@coana-tech/cli",
"version": "14.12.139"
"version": "14.12.148"
},
"@cyclonedx/cdxgen": {
"description": "CycloneDX SBOM generator for software bill of materials",
Expand All @@ -24,6 +24,12 @@
"package": "socketsecurity",
"version": "^2.2.15"
},
"socket-patch": {
"description": "Socket Patch CLI for applying security patches",
"type": "npm",
"package": "@socketsecurity/socket-patch",
"version": "1.2.0"
},
"sfw": {
"description": "Socket Firewall (sfw)",
"type": "standalone",
Expand Down
45 changes: 45 additions & 0 deletions packages/cli/src/commands/patch-old/cmd-patch.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { cmdPatchApply } from './cmd-patch-apply.mts'
import { cmdPatchCleanup } from './cmd-patch-cleanup.mts'
import { cmdPatchDiscover } from './cmd-patch-discover.mts'
import { cmdPatchDownload } from './cmd-patch-download.mts'
import { cmdPatchGet } from './cmd-patch-get.mts'
import { cmdPatchInfo } from './cmd-patch-info.mts'
import { cmdPatchList } from './cmd-patch-list.mts'
import { cmdPatchRm } from './cmd-patch-rm.mts'
import { cmdPatchStatus } from './cmd-patch-status.mts'
import { meowWithSubcommands } from '../../utils/cli/with-subcommands.mjs'

import type { CliSubcommand } from '../../utils/cli/with-subcommands.mjs'

const description = 'Manage CVE patches for dependencies'

const hidden = false

export const cmdPatch: CliSubcommand = {
description,
hidden,
async run(argv, importMeta, { parentName }) {
await meowWithSubcommands(
{
argv,
name: `${parentName} patch`,
importMeta,
subcommands: {
apply: cmdPatchApply,
cleanup: cmdPatchCleanup,
discover: cmdPatchDiscover,
download: cmdPatchDownload,
get: cmdPatchGet,
info: cmdPatchInfo,
list: cmdPatchList,
rm: cmdPatchRm,
status: cmdPatchStatus,
},
},
{
defaultSub: 'discover',
description,
},
)
},
}
54 changes: 19 additions & 35 deletions packages/cli/src/commands/patch/cmd-patch.mts
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { cmdPatchApply } from './cmd-patch-apply.mts'
import { cmdPatchCleanup } from './cmd-patch-cleanup.mts'
import { cmdPatchDiscover } from './cmd-patch-discover.mts'
import { cmdPatchDownload } from './cmd-patch-download.mts'
import { cmdPatchGet } from './cmd-patch-get.mts'
import { cmdPatchInfo } from './cmd-patch-info.mts'
import { cmdPatchList } from './cmd-patch-list.mts'
import { cmdPatchRm } from './cmd-patch-rm.mts'
import { cmdPatchStatus } from './cmd-patch-status.mts'
import { meowWithSubcommands } from '../../utils/cli/with-subcommands.mjs'
import { spawnSocketPatch } from '../../utils/socket-patch/spawn.mts'

import type { CliSubcommand } from '../../utils/cli/with-subcommands.mjs'
import type {
CliCommandContext,
CliSubcommand,
} from '../../utils/cli/with-subcommands.mjs'

const description = 'Manage CVE patches for dependencies'

Expand All @@ -18,28 +12,18 @@ const hidden = false
export const cmdPatch: CliSubcommand = {
description,
hidden,
async run(argv, importMeta, { parentName }) {
await meowWithSubcommands(
{
argv,
name: `${parentName} patch`,
importMeta,
subcommands: {
apply: cmdPatchApply,
cleanup: cmdPatchCleanup,
discover: cmdPatchDiscover,
download: cmdPatchDownload,
get: cmdPatchGet,
info: cmdPatchInfo,
list: cmdPatchList,
rm: cmdPatchRm,
status: cmdPatchStatus,
},
},
{
defaultSub: 'discover',
description,
},
)
},
run,
}

async function run(
argv: string[] | readonly string[],
_importMeta: ImportMeta,
_context: CliCommandContext,
): Promise<void> {
// Forward all arguments to socket-patch.
const result = await spawnSocketPatch([...argv])

if (!result.ok) {
process.exitCode = 1
}
}
6 changes: 6 additions & 0 deletions packages/cli/src/constants/env.mts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { SOCKET_CLI_BOOTSTRAP_SPEC } from '../env/socket-cli-bootstrap-spec.mts'
import { SOCKET_CLI_CDXGEN_LOCAL_PATH } from '../env/socket-cli-cdxgen-local-path.mts'
import { SOCKET_CLI_COANA_LOCAL_PATH } from '../env/socket-cli-coana-local-path.mts'
import { SOCKET_CLI_CONFIG } from '../env/socket-cli-config.mts'
import { SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH } from '../env/socket-cli-socket-patch-local-path.mts'
import { SOCKET_CLI_DEBUG } from '../env/socket-cli-debug.mts'
import { SOCKET_CLI_FIX } from '../env/socket-cli-fix.mts'
import { SOCKET_CLI_GIT_USER_EMAIL } from '../env/socket-cli-git-user-email.mts'
Expand All @@ -64,6 +65,7 @@ import { SOCKET_CLI_SEA_NODE_VERSION } from '../env/socket-cli-sea-node-version.
import { SOCKET_CLI_SFW_LOCAL_PATH } from '../env/socket-cli-sfw-local-path.mts'
import { SOCKET_CLI_SKIP_UPDATE_CHECK } from '../env/socket-cli-skip-update-check.mts'
import { SOCKET_CLI_VIEW_ALL_RISKS } from '../env/socket-cli-view-all-risks.mts'
import { getSocketPatchVersion } from '../env/socket-patch-version.mts'
import { getSynpVersion } from '../env/synp-version.mts'
import { TEMP } from '../env/temp.mts'
import { TERM } from '../env/term.mts'
Expand Down Expand Up @@ -122,6 +124,7 @@ export {
SOCKET_CLI_SEA_NODE_VERSION,
SOCKET_CLI_SFW_LOCAL_PATH,
SOCKET_CLI_SKIP_UPDATE_CHECK,
SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH,
SOCKET_CLI_VIEW_ALL_RISKS,
TEMP,
TERM,
Expand All @@ -143,6 +146,7 @@ export {
getPyCliVersion,
getPythonBuildTag,
getPythonVersion,
getSocketPatchVersion,
getSynpVersion,
isPublishedBuild,
isSentryBuild,
Expand Down Expand Up @@ -202,6 +206,7 @@ const envSnapshot = {
SOCKET_CLI_SEA_NODE_VERSION,
SOCKET_CLI_SFW_LOCAL_PATH,
SOCKET_CLI_SKIP_UPDATE_CHECK,
SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH,
SOCKET_CLI_VIEW_ALL_RISKS,
TEMP,
TERM,
Expand All @@ -222,6 +227,7 @@ const envSnapshot = {
INLINED_SOCKET_CLI_PYCLI_VERSION: getPyCliVersion(),
INLINED_SOCKET_CLI_SENTRY_BUILD: isSentryBuild(),
INLINED_SOCKET_CLI_SFW_VERSION: getSwfVersion(),
INLINED_SOCKET_CLI_SOCKET_PATCH_VERSION: getSocketPatchVersion(),
INLINED_SOCKET_CLI_SYNP_VERSION: getSynpVersion(),
INLINED_SOCKET_CLI_VERSION: getCliVersion(),
INLINED_SOCKET_CLI_VERSION_HASH: getCliVersionHash(),
Expand Down
8 changes: 7 additions & 1 deletion packages/cli/src/env/coana-version.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@
import process from 'node:process'

export function getCoanaVersion(): string {
return process.env['INLINED_SOCKET_CLI_COANA_VERSION'] ?? ''
const version = process.env['INLINED_SOCKET_CLI_COANA_VERSION']
if (!version) {
throw new Error(
'INLINED_SOCKET_CLI_COANA_VERSION not found. Please ensure @coana-tech/cli is properly configured in external-tools.json.',
)
}
return version
}
8 changes: 7 additions & 1 deletion packages/cli/src/env/sfw-version.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,11 @@
import process from 'node:process'

export function getSwfVersion(): string {
return process.env['INLINED_SOCKET_CLI_SFW_VERSION'] ?? ''
const version = process.env['INLINED_SOCKET_CLI_SFW_VERSION']
if (!version) {
throw new Error(
'INLINED_SOCKET_CLI_SFW_VERSION not found. Please ensure sfw is properly configured in external-tools.json.',
)
}
return version
}
14 changes: 14 additions & 0 deletions packages/cli/src/env/socket-cli-socket-patch-local-path.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { envAsString } from '@socketsecurity/lib/env'

/**
* Override the socket-patch path for local development/testing.
* When set, uses the local socket-patch CLI instead of downloading from npm.
*
* @example
* ```bash
* export SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH=/path/to/socket-patch/bin/cli.js
* ```
*/
export const SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH = envAsString(
process.env['SOCKET_CLI_SOCKET_PATCH_LOCAL_PATH'],
)
19 changes: 19 additions & 0 deletions packages/cli/src/env/socket-patch-version.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Socket Patch version getter function.
* Uses direct process.env access so esbuild define can inline values.
* IMPORTANT: esbuild's define plugin can only replace direct process.env['KEY'] references.
* If we imported from env modules, esbuild couldn't inline the values at build time.
* This is critical for embedding version info into the binary.
*/

import process from 'node:process'

export function getSocketPatchVersion(): string {
const version = process.env['INLINED_SOCKET_CLI_SOCKET_PATCH_VERSION']
if (!version) {
throw new Error(
'INLINED_SOCKET_CLI_SOCKET_PATCH_VERSION not found. Please ensure socket-patch is properly configured in external-tools.json.',
)
}
return version
}
45 changes: 35 additions & 10 deletions packages/cli/src/utils/fs/glob.mts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,14 @@ export function filterBySupportedScanFiles(
return filepaths.filter(p => micromatch.some(p, patterns, { dot: true }))
}

export function createSupportedFilesFilter(
supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'],
): (filepath: string) => boolean {
const patterns = getSupportedFilePatterns(supportedFiles)
return (filepath: string) =>
micromatch.some(filepath, patterns, { dot: true })
}

export function getSupportedFilePatterns(
supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'],
): string[] {
Expand All @@ -179,6 +187,10 @@ export function getSupportedFilePatterns(
}

type GlobWithGitIgnoreOptions = GlobOptions & {
// Optional filter function to apply during streaming.
// When provided, only files passing this filter are accumulated.
// This is critical for memory efficiency when scanning large monorepos.
filter?: ((filepath: string) => boolean) | undefined
socketConfig?: SocketYml | undefined
}

Expand All @@ -188,6 +200,7 @@ export async function globWithGitIgnore(
): Promise<string[]> {
const {
cwd = process.cwd(),
filter,
socketConfig,
...additionalOptions
} = { __proto__: null, ...options } as GlobWithGitIgnoreOptions
Expand Down Expand Up @@ -244,27 +257,39 @@ export async function globWithGitIgnore(
...additionalOptions,
} as GlobOptions

if (!hasNegatedPattern) {
// When no filter is provided and no negated patterns exist, use the fast path.
if (!hasNegatedPattern && !filter) {
return await fastGlob.glob(patterns as string[], globOptions)
}

// Add support for negated "ignore" patterns which many globbing libraries,
// including 'fast-glob', 'globby', and 'tinyglobby', lack support for.
const filtered: string[] = []
const ig = ignore().add([...ignores])
// Use streaming to avoid unbounded memory accumulation.
// This is critical for large monorepos with 100k+ files.
const results: string[] = []
const ig = hasNegatedPattern ? ignore().add([...ignores]) : null
const stream = fastGlob.globStream(
patterns as string[],
globOptions,
) as AsyncIterable<string>
for await (const p of stream) {
// Note: the input files must be INSIDE the cwd. If you get strange looking
// relative path errors here, most likely your path is outside the given cwd.
const relPath = globOptions.absolute ? path.relative(cwd, p) : p
if (!ig.ignores(relPath)) {
filtered.push(p)
// Check gitignore patterns with negation support.
if (ig) {
// Note: the input files must be INSIDE the cwd. If you get strange looking
// relative path errors here, most likely your path is outside the given cwd.
const relPath = globOptions.absolute ? path.relative(cwd, p) : p
if (ig.ignores(relPath)) {
continue
}
}
// Apply the optional filter to reduce memory usage.
// When scanning large monorepos, this filters early (e.g., to manifest files only)
// instead of accumulating all 100k+ files and filtering later.
if (filter && !filter(p)) {
continue
}
results.push(p)
}
return filtered
return results
}

export async function globWorkspace(
Expand Down
12 changes: 8 additions & 4 deletions packages/cli/src/utils/fs/path-resolve.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { WIN32 } from '@socketsecurity/lib/constants/platform'
import { isDirSync } from '@socketsecurity/lib/fs'

import {
filterBySupportedScanFiles,
createSupportedFilesFilter,
globWithGitIgnore,
pathsToGlobPatterns,
} from './glob.mts'
Expand Down Expand Up @@ -127,13 +127,17 @@ export async function getPackageFilesForScan(
...options,
} as PackageFilesForScanOptions

const filepaths = await globWithGitIgnore(
// Apply the supported files filter during streaming to avoid accumulating
// all files in memory. This is critical for large monorepos with 100k+ files
// where accumulating all paths before filtering causes OOM errors.
const filter = createSupportedFilesFilter(supportedFiles)

return await globWithGitIgnore(
pathsToGlobPatterns(inputPaths, options?.cwd),
{
cwd,
filter,
socketConfig,
},
)

return filterBySupportedScanFiles(filepaths!, supportedFiles)
}
Loading
Loading