diff --git a/CHANGELOG.md b/CHANGELOG.md index 77314db63..2739245a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ 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/). +## [1.1.128](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.128) - 2026-06-25 + +### Changed +- Updated the Coana CLI to v `15.5.10`. + +### Fixed +- Scans now skip Python virtual environments when collecting manifest files. Folders named `.venv`, and any folder containing a `pyvenv.cfg` marker (covering `venv`, `env`, and custom-named environments), are excluded — so `socket scan`, reachability, and `socket fix` stay focused on your project's own manifests instead of the thousands installed inside a virtualenv. + ## [1.1.127](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.127) - 2026-06-24 ### Changed diff --git a/package.json b/package.json index c322f10c5..29e49b619 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.127", + "version": "1.1.128", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT", @@ -96,7 +96,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.5.9", + "@coana-tech/cli": "15.5.10", "@cyclonedx/cdxgen": "12.1.2", "@dotenvx/dotenvx": "1.49.0", "@eslint/compat": "1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92af2a5fc..2babb4901 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.5.9 - version: 15.5.9 + specifier: 15.5.10 + version: 15.5.10 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -749,8 +749,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.5.9': - resolution: {integrity: sha512-IZd+XArq7IeIHGZTIOfrz/fIKfoSwTS7TYIjLxSSqsHTiRF2X2btjJsAh0je/4hv+thRSA0SROlVLm0sKKAvZQ==} + '@coana-tech/cli@15.5.10': + resolution: {integrity: sha512-DIZp+He1jvCczy8MIgVhq/WWeUqdvFYF59mRaBNXo3HTJNnV4Lj5OsudeeLANhLr6dfq5pZBxNT32G9Er9JSmw==} hasBin: true '@colors/colors@1.5.0': @@ -5385,7 +5385,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.5.9': {} + '@coana-tech/cli@15.5.10': {} '@colors/colors@1.5.0': optional: true diff --git a/src/utils/glob.mts b/src/utils/glob.mts index e24cf54c7..21c3838fd 100644 --- a/src/utils/glob.mts +++ b/src/utils/glob.mts @@ -36,10 +36,18 @@ export const IGNORED_DIRS = [ // Taken from globby: // https://github.com/sindresorhus/globby/blob/v14.0.2/ignore.js#L11-L16 'flow-typed', + // Conventional Python virtual environment dir. Arbitrarily-named venvs are + // detected via their pyvenv.cfg marker during the discovery walk below. + '.venv', ] as const const IGNORED_DIR_PATTERNS = IGNORED_DIRS.map(i => `**/${i}`) +// Marker file at the root of every Python virtual environment (stdlib `venv` +// per PEP 405, and virtualenv >= 20). Lets us detect venvs that don't use a +// conventional directory name. +const PYVENV_CFG = 'pyvenv.cfg' + async function getWorkspaceGlobs( agent: Agent, cwd = process.cwd(), @@ -251,38 +259,53 @@ export async function globWithGitIgnore( ignores.add(pattern) } - // The .gitignore discovery walk has to honor the same directory exclusions - // as the package walk below. Otherwise an unreadable subtree (e.g. a - // postgres `pgdata` dir owned by another uid, or a Docker volume mount) makes - // fast-glob throw `EACCES: permission denied, scandir` *here* — before - // --exclude-paths (`cliMinimatchIgnores`) or projectIgnorePaths are ever - // applied to the main walk, which is why excluding the path did not help. - // `suppressErrors` is the backstop: a directory the user simply cannot read - // cannot contain manifests they could scan anyway, so skip it instead of - // aborting the whole `socket fix` / `socket scan` run. Negated patterns are - // dropped — for a discovery walk they could only re-include a subtree (never - // prevent a crash), and fast-glob treats `!` ignore entries inconsistently. - const gitIgnoreStream = fastGlob.globStream(['**/.gitignore'], { - absolute: true, - cwd, - dot: true, - ignore: [ - ...DEFAULT_IGNORE_FOR_GIT_IGNORE, - ...projectIgnoreGlobs, - ...cliMinimatchIgnores, - ] - .filter(p => p.charCodeAt(0) !== 33 /*'!'*/) - .map(stripTrailingSlash), - suppressErrors: true, - }) + // The discovery walk (`.gitignore` files plus `pyvenv.cfg` venv markers) has + // to honor the same directory exclusions as the package walk below. Otherwise + // an unreadable subtree (e.g. a postgres `pgdata` dir owned by another uid, or + // a Docker volume mount) makes fast-glob throw `EACCES: permission denied, + // scandir` *here* — before --exclude-paths (`cliMinimatchIgnores`) or + // projectIgnorePaths are ever applied to the main walk, which is why excluding + // the path did not help. `suppressErrors` is the backstop: a directory the + // user simply cannot read cannot contain manifests they could scan anyway, so + // skip it instead of aborting the whole `socket fix` / `socket scan` run. + // Negated patterns are dropped — for a discovery walk they could only + // re-include a subtree (never prevent a crash), and fast-glob treats `!` + // ignore entries inconsistently. Folding pyvenv.cfg discovery into this same + // walk avoids a second full-tree traversal. + const discoveryStream = fastGlob.globStream( + ['**/.gitignore', `**/${PYVENV_CFG}`], + { + absolute: true, + cwd, + dot: true, + ignore: [ + ...DEFAULT_IGNORE_FOR_GIT_IGNORE, + ...projectIgnoreGlobs, + ...cliMinimatchIgnores, + ] + .filter(p => p.charCodeAt(0) !== 33 /*'!'*/) + .map(stripTrailingSlash), + suppressErrors: true, + }, + ) for await (const ignorePatterns of transform( - gitIgnoreStream, - async (filepath: string) => - ignoreFileToGlobPatterns( + discoveryStream, + async (filepath: string) => { + if (path.basename(filepath) === PYVENV_CFG) { + // A pyvenv.cfg sits at the venv root, so exclude the whole directory. + const relDir = path + .relative(cwd, path.dirname(filepath)) + .replace(/\\/g, '/') + // An empty relDir means the scan target itself is a venv root; don't + // emit `/**`, which would exclude everything the user explicitly targeted. + return relDir ? [`${relDir}/**`] : [] + } + return ignoreFileToGlobPatterns( (await safeReadFile(filepath)) ?? '', filepath, cwd, - ), + ) + }, { concurrency: 8 }, )) { for (const p of ignorePatterns) { diff --git a/src/utils/glob.test.mts b/src/utils/glob.test.mts index f403306cd..9cbd3c836 100644 --- a/src/utils/glob.test.mts +++ b/src/utils/glob.test.mts @@ -321,6 +321,79 @@ describe('glob utilities', () => { } }, ) + + it('excludes a Python virtual environment detected via pyvenv.cfg', async () => { + // A venv can use any directory name; the reliable signal is the + // pyvenv.cfg marker at its root. Manifests inside it must not surface. + mockTestFs({ + [`${mockFixturePath}/requirements.txt`]: '', + [`${mockFixturePath}/myenv/pyvenv.cfg`]: 'home = /usr/bin\nversion = 3.11.0\n', + [`${mockFixturePath}/myenv/requirements.txt`]: '', + [`${mockFixturePath}/myenv/lib/python3.11/site-packages/foo/setup.py`]: + '', + }) + + const results = await globWithGitIgnore( + ['**/requirements.txt', '**/setup.py'], + { cwd: mockFixturePath }, + ) + + expect(results.map(normalizePath).sort()).toEqual([ + `${mockFixturePath}/requirements.txt`, + ]) + }) + + it('excludes a `.venv` directory by name', async () => { + mockTestFs({ + [`${mockFixturePath}/package.json`]: '{}', + [`${mockFixturePath}/.venv/lib/site-packages/foo/package.json`]: '{}', + }) + + const results = await globWithGitIgnore(['**/*.json'], { + cwd: mockFixturePath, + }) + + expect(results.map(normalizePath).sort()).toEqual([ + `${mockFixturePath}/package.json`, + ]) + }) + + it('keeps a non-venv directory named `venv` without a pyvenv.cfg', async () => { + // Guards against over-exclusion: a bare `venv` dir is only skipped when + // it actually contains a pyvenv.cfg, never by name alone. + mockTestFs({ + [`${mockFixturePath}/package.json`]: '{}', + [`${mockFixturePath}/venv/package.json`]: '{}', + }) + + const results = await globWithGitIgnore(['**/*.json'], { + cwd: mockFixturePath, + }) + + expect(results.map(normalizePath).sort()).toEqual([ + `${mockFixturePath}/package.json`, + `${mockFixturePath}/venv/package.json`, + ]) + }) + + it('excludes a venv via pyvenv.cfg through the streaming filter path', async () => { + // The actual manifest-scan path always passes a filter, so verify the + // venv exclusion prunes there too. + mockTestFs({ + [`${mockFixturePath}/package.json`]: '{}', + [`${mockFixturePath}/env/pyvenv.cfg`]: 'home = /usr/bin\n', + [`${mockFixturePath}/env/lib/site-packages/bar/package.json`]: '{}', + }) + + const results = await globWithGitIgnore(['**/*'], { + cwd: mockFixturePath, + filter: filterJsonFiles, + }) + + expect(results.map(normalizePath).sort()).toEqual([ + `${mockFixturePath}/package.json`, + ]) + }) }) describe('createSupportedFilesFilter()', () => {