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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

79 changes: 51 additions & 28 deletions src/utils/glob.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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) {
Expand Down
73 changes: 73 additions & 0 deletions src/utils/glob.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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()', () => {
Expand Down
Loading