Skip to content

Commit 330612d

Browse files
authored
fix(scan): exclude Python virtual environments from manifest collection + bump Coana CLI to 15.5.10 (1.1.128) (#1379)
* fix(scan): exclude Python virtual environments from manifest collection (1.1.128) Recursive manifest discovery for `socket scan`, reachability, and `socket fix` walked into Python virtual environments and collected the thousands of dependency manifests (setup.py, pyproject.toml, requirements.txt, …) installed under their site-packages, bloating scans with packages that are not part of the user's project. Exclude venvs two ways: - Add `.venv` to IGNORED_DIRS for a cheap traversal-prune of the conventional directory name. - Detect arbitrarily-named venvs by their `pyvenv.cfg` marker (written at the environment root by stdlib `venv` per PEP 405 and by virtualenv >= 20). Discovery is folded into the existing `.gitignore` discovery walk, so it adds no extra full-tree traversal; each venv root contributes a `<dir>/**` ignore that all downstream glob paths honor. Bare `venv`/`env` are intentionally not name-excluded to avoid skipping a legitimately-named non-venv directory; the pyvenv.cfg check covers them. * chore: bump Coana CLI to 15.5.10
1 parent cb38374 commit 330612d

5 files changed

Lines changed: 139 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
44

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

7+
## [1.1.128](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.128) - 2026-06-25
8+
9+
### Changed
10+
- Updated the Coana CLI to v `15.5.10`.
11+
12+
### Fixed
13+
- 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.
14+
715
## [1.1.127](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.127) - 2026-06-24
816

917
### Changed

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "socket",
3-
"version": "1.1.127",
3+
"version": "1.1.128",
44
"description": "CLI for Socket.dev",
55
"homepage": "https://github.com/SocketDev/socket-cli",
66
"license": "MIT",
@@ -96,7 +96,7 @@
9696
"@babel/preset-typescript": "7.27.1",
9797
"@babel/runtime": "7.28.4",
9898
"@biomejs/biome": "2.2.4",
99-
"@coana-tech/cli": "15.5.9",
99+
"@coana-tech/cli": "15.5.10",
100100
"@cyclonedx/cdxgen": "12.1.2",
101101
"@dotenvx/dotenvx": "1.49.0",
102102
"@eslint/compat": "1.3.2",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/utils/glob.mts

Lines changed: 51 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,18 @@ export const IGNORED_DIRS = [
3636
// Taken from globby:
3737
// https://github.com/sindresorhus/globby/blob/v14.0.2/ignore.js#L11-L16
3838
'flow-typed',
39+
// Conventional Python virtual environment dir. Arbitrarily-named venvs are
40+
// detected via their pyvenv.cfg marker during the discovery walk below.
41+
'.venv',
3942
] as const
4043

4144
const IGNORED_DIR_PATTERNS = IGNORED_DIRS.map(i => `**/${i}`)
4245

46+
// Marker file at the root of every Python virtual environment (stdlib `venv`
47+
// per PEP 405, and virtualenv >= 20). Lets us detect venvs that don't use a
48+
// conventional directory name.
49+
const PYVENV_CFG = 'pyvenv.cfg'
50+
4351
async function getWorkspaceGlobs(
4452
agent: Agent,
4553
cwd = process.cwd(),
@@ -251,38 +259,53 @@ export async function globWithGitIgnore(
251259
ignores.add(pattern)
252260
}
253261

254-
// The .gitignore discovery walk has to honor the same directory exclusions
255-
// as the package walk below. Otherwise an unreadable subtree (e.g. a
256-
// postgres `pgdata` dir owned by another uid, or a Docker volume mount) makes
257-
// fast-glob throw `EACCES: permission denied, scandir` *here* — before
258-
// --exclude-paths (`cliMinimatchIgnores`) or projectIgnorePaths are ever
259-
// applied to the main walk, which is why excluding the path did not help.
260-
// `suppressErrors` is the backstop: a directory the user simply cannot read
261-
// cannot contain manifests they could scan anyway, so skip it instead of
262-
// aborting the whole `socket fix` / `socket scan` run. Negated patterns are
263-
// dropped — for a discovery walk they could only re-include a subtree (never
264-
// prevent a crash), and fast-glob treats `!` ignore entries inconsistently.
265-
const gitIgnoreStream = fastGlob.globStream(['**/.gitignore'], {
266-
absolute: true,
267-
cwd,
268-
dot: true,
269-
ignore: [
270-
...DEFAULT_IGNORE_FOR_GIT_IGNORE,
271-
...projectIgnoreGlobs,
272-
...cliMinimatchIgnores,
273-
]
274-
.filter(p => p.charCodeAt(0) !== 33 /*'!'*/)
275-
.map(stripTrailingSlash),
276-
suppressErrors: true,
277-
})
262+
// The discovery walk (`.gitignore` files plus `pyvenv.cfg` venv markers) has
263+
// to honor the same directory exclusions as the package walk below. Otherwise
264+
// an unreadable subtree (e.g. a postgres `pgdata` dir owned by another uid, or
265+
// a Docker volume mount) makes fast-glob throw `EACCES: permission denied,
266+
// scandir` *here* — before --exclude-paths (`cliMinimatchIgnores`) or
267+
// projectIgnorePaths are ever applied to the main walk, which is why excluding
268+
// the path did not help. `suppressErrors` is the backstop: a directory the
269+
// user simply cannot read cannot contain manifests they could scan anyway, so
270+
// skip it instead of aborting the whole `socket fix` / `socket scan` run.
271+
// Negated patterns are dropped — for a discovery walk they could only
272+
// re-include a subtree (never prevent a crash), and fast-glob treats `!`
273+
// ignore entries inconsistently. Folding pyvenv.cfg discovery into this same
274+
// walk avoids a second full-tree traversal.
275+
const discoveryStream = fastGlob.globStream(
276+
['**/.gitignore', `**/${PYVENV_CFG}`],
277+
{
278+
absolute: true,
279+
cwd,
280+
dot: true,
281+
ignore: [
282+
...DEFAULT_IGNORE_FOR_GIT_IGNORE,
283+
...projectIgnoreGlobs,
284+
...cliMinimatchIgnores,
285+
]
286+
.filter(p => p.charCodeAt(0) !== 33 /*'!'*/)
287+
.map(stripTrailingSlash),
288+
suppressErrors: true,
289+
},
290+
)
278291
for await (const ignorePatterns of transform(
279-
gitIgnoreStream,
280-
async (filepath: string) =>
281-
ignoreFileToGlobPatterns(
292+
discoveryStream,
293+
async (filepath: string) => {
294+
if (path.basename(filepath) === PYVENV_CFG) {
295+
// A pyvenv.cfg sits at the venv root, so exclude the whole directory.
296+
const relDir = path
297+
.relative(cwd, path.dirname(filepath))
298+
.replace(/\\/g, '/')
299+
// An empty relDir means the scan target itself is a venv root; don't
300+
// emit `/**`, which would exclude everything the user explicitly targeted.
301+
return relDir ? [`${relDir}/**`] : []
302+
}
303+
return ignoreFileToGlobPatterns(
282304
(await safeReadFile(filepath)) ?? '',
283305
filepath,
284306
cwd,
285-
),
307+
)
308+
},
286309
{ concurrency: 8 },
287310
)) {
288311
for (const p of ignorePatterns) {

src/utils/glob.test.mts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,79 @@ describe('glob utilities', () => {
321321
}
322322
},
323323
)
324+
325+
it('excludes a Python virtual environment detected via pyvenv.cfg', async () => {
326+
// A venv can use any directory name; the reliable signal is the
327+
// pyvenv.cfg marker at its root. Manifests inside it must not surface.
328+
mockTestFs({
329+
[`${mockFixturePath}/requirements.txt`]: '',
330+
[`${mockFixturePath}/myenv/pyvenv.cfg`]: 'home = /usr/bin\nversion = 3.11.0\n',
331+
[`${mockFixturePath}/myenv/requirements.txt`]: '',
332+
[`${mockFixturePath}/myenv/lib/python3.11/site-packages/foo/setup.py`]:
333+
'',
334+
})
335+
336+
const results = await globWithGitIgnore(
337+
['**/requirements.txt', '**/setup.py'],
338+
{ cwd: mockFixturePath },
339+
)
340+
341+
expect(results.map(normalizePath).sort()).toEqual([
342+
`${mockFixturePath}/requirements.txt`,
343+
])
344+
})
345+
346+
it('excludes a `.venv` directory by name', async () => {
347+
mockTestFs({
348+
[`${mockFixturePath}/package.json`]: '{}',
349+
[`${mockFixturePath}/.venv/lib/site-packages/foo/package.json`]: '{}',
350+
})
351+
352+
const results = await globWithGitIgnore(['**/*.json'], {
353+
cwd: mockFixturePath,
354+
})
355+
356+
expect(results.map(normalizePath).sort()).toEqual([
357+
`${mockFixturePath}/package.json`,
358+
])
359+
})
360+
361+
it('keeps a non-venv directory named `venv` without a pyvenv.cfg', async () => {
362+
// Guards against over-exclusion: a bare `venv` dir is only skipped when
363+
// it actually contains a pyvenv.cfg, never by name alone.
364+
mockTestFs({
365+
[`${mockFixturePath}/package.json`]: '{}',
366+
[`${mockFixturePath}/venv/package.json`]: '{}',
367+
})
368+
369+
const results = await globWithGitIgnore(['**/*.json'], {
370+
cwd: mockFixturePath,
371+
})
372+
373+
expect(results.map(normalizePath).sort()).toEqual([
374+
`${mockFixturePath}/package.json`,
375+
`${mockFixturePath}/venv/package.json`,
376+
])
377+
})
378+
379+
it('excludes a venv via pyvenv.cfg through the streaming filter path', async () => {
380+
// The actual manifest-scan path always passes a filter, so verify the
381+
// venv exclusion prunes there too.
382+
mockTestFs({
383+
[`${mockFixturePath}/package.json`]: '{}',
384+
[`${mockFixturePath}/env/pyvenv.cfg`]: 'home = /usr/bin\n',
385+
[`${mockFixturePath}/env/lib/site-packages/bar/package.json`]: '{}',
386+
})
387+
388+
const results = await globWithGitIgnore(['**/*'], {
389+
cwd: mockFixturePath,
390+
filter: filterJsonFiles,
391+
})
392+
393+
expect(results.map(normalizePath).sort()).toEqual([
394+
`${mockFixturePath}/package.json`,
395+
])
396+
})
324397
})
325398

326399
describe('createSupportedFilesFilter()', () => {

0 commit comments

Comments
 (0)