From 493701a307b9e89f15436fda2facd180d6abf83e Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 23 Apr 2026 11:13:23 -0400 Subject: [PATCH 1/4] fix(cli): stop socket cdxgen from silently shipping empty-components SBOMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When `socket cdxgen` runs with its safe defaults (`--lifecycle pre-build` + `--no-install-deps`) against a Node.js project that has neither a lockfile nor an installed `node_modules/`, cdxgen produces a structurally-valid CycloneDX document with `"components": []`. The Socket dashboard ingests the SBOM cleanly but has no dependency data to render, so the Alerts tab shows nothing — indistinguishable from a genuinely clean repo. Add two gates to the command: * Hard gate in cmd-manifest-cdxgen.mts: when the default lifecycle path kicks in for a Node.js type and neither a lockfile nor node_modules/ is findable upward from cwd, refuse with exit code 2 and an actionable message (install first or pass --lifecycle build). Skips the gate when the user passes --lifecycle, --filter, --only, or a non-Node --type. * Soft gate in run-cdxgen.mts: after cdxgen writes its output, parse it and warn loudly when `components` is empty. Covers configurations that slip past the hard gate (overly narrow --filter/--only, ecosystem mismatch, etc.) so an empty SBOM cannot ship silently. The detection helpers (`detectNodejsCdxgenSources`, `isNodejsCdxgenType`) are exported for unit testing and accept a `cwd` override so the probe can be exercised without relying on process.chdir (not supported in vitest workers). --- CHANGELOG.md | 1 + .../commands/manifest/cmd-manifest-cdxgen.mts | 32 +++++- .../cli/src/commands/manifest/run-cdxgen.mts | 91 ++++++++++++++- .../manifest/cmd-manifest-cdxgen.test.mts | 105 ++++++++++++++++++ .../commands/manifest/run-cdxgen.test.mts | 90 +++++++++++++++ 5 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts diff --git a/CHANGELOG.md b/CHANGELOG.md index fe93ae61d..52c7afabe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - Prevent heap overflow in large monorepo scans by using streaming-based filtering to avoid accumulating all file paths in memory before filtering. - `socket scan create` now rejects `--default-branch=` and `--default-branch ` (space-separated) with an actionable error instead of silently dropping the branch name. Scans that used the misuse shape were getting recorded without a branch tag and disappearing from the Main/PR dashboard tabs. - `socket repository create` / `socket repository update` now reject bare `--default-branch` (no value) and `--default-branch=` (empty value). Previously both persisted a blank default-branch name on the repo record. +- `socket cdxgen` no longer silently produces SBOMs with an empty `components` array when run in the default `--lifecycle pre-build` + `--no-install-deps` mode against a Node.js project that has no lockfile and no `node_modules/`. The command now fails fast with an actionable error (install dependencies or pass `--lifecycle build`), and when the generated BOM still ends up empty for any other reason (e.g. overly narrow `--filter`/`--only`), emits a post-run warning so the condition is surfaced instead of shipping an SBOM that renders as "no alerts" on the Socket dashboard. ## [2.1.0](https://github.com/SocketDev/socket-cli/releases/tag/v2.1.0) - 2025-11-02 diff --git a/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts b/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts index 6e5c3bebc..c869f0d15 100644 --- a/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts +++ b/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts @@ -6,7 +6,11 @@ import { getDefaultLogger } from '@socketsecurity/lib/logger' import { isPath } from '@socketsecurity/lib/paths/normalize' import { pluralize } from '@socketsecurity/lib/words' -import { runCdxgen } from './run-cdxgen.mts' +import { + detectNodejsCdxgenSources, + isNodejsCdxgenType, + runCdxgen, +} from './run-cdxgen.mts' import { FLAG_HELP } from '../../constants/cli.mjs' import { outputDryRunExecute } from '../../utils/dry-run/output.mts' import { commonFlags, outputFlags } from '../../flags.mts' @@ -285,7 +289,8 @@ async function run( // Make 'lifecycle' default to 'pre-build', which also sets 'install-deps' to `false`, // to avoid arbitrary code execution on the cdxgen scan. // https://github.com/CycloneDX/cdxgen/issues/1328 - if (yargv.lifecycle === undefined) { + const lifecycleWasDefaulted = yargv.lifecycle === undefined + if (lifecycleWasDefaulted) { yargv.lifecycle = 'pre-build' yargv['install-deps'] = false logger.info( @@ -298,6 +303,29 @@ async function run( if (yargv.output === undefined) { yargv.output = 'socket-cdx.json' } + + // Hard gate: in the default pre-build + install-deps=false path, cdxgen + // needs either a lockfile or an installed node_modules/ to produce any + // Node.js components. Without both, it emits a valid CycloneDX doc with + // "components": []. Refuse with an actionable error instead of shipping + // an empty SBOM (SMO-590). + if ( + lifecycleWasDefaulted && + isNodejsCdxgenType(yargv.type) && + !yargv['filter'] && + !yargv['only'] + ) { + const { hasLockfile, hasNodeModules } = await detectNodejsCdxgenSources() + if (!hasLockfile && !hasNodeModules) { + process.exitCode = 2 + logger.fail( + `socket cdxgen found no lockfile (pnpm-lock.yaml / package-lock.json / yarn.lock) or node_modules/ at or above ${process.cwd()}.\n` + + ' The default --lifecycle pre-build with --no-install-deps needs one of them to resolve components; otherwise the SBOM ships with "components": [].\n' + + ' Fix: install dependencies first (e.g. `npm install`, `pnpm install`, `yarn install`), or re-run with `--lifecycle build` to let cdxgen resolve during the build.', + ) + return + } + } } process.exitCode = 1 diff --git a/packages/cli/src/commands/manifest/run-cdxgen.mts b/packages/cli/src/commands/manifest/run-cdxgen.mts index 93046f8fb..7702d4d2b 100644 --- a/packages/cli/src/commands/manifest/run-cdxgen.mts +++ b/packages/cli/src/commands/manifest/run-cdxgen.mts @@ -1,4 +1,4 @@ -import { existsSync } from 'node:fs' +import { existsSync, promises as fs } from 'node:fs' import path from 'node:path' import colors from 'yoctocolors-cjs' @@ -8,6 +8,7 @@ import { safeDeleteSync } from '@socketsecurity/lib/fs' import { getDefaultLogger } from '@socketsecurity/lib/logger' import { FLAG_HELP } from '../../constants/cli.mjs' +import { NODE_MODULES } from '../../constants/packages.mts' import { PACKAGE_LOCK_JSON, PNPM_LOCK_YAML, @@ -32,6 +33,55 @@ const nodejsPlatformTypes = new Set([ 'typescript', ]) +/** + * Result of probing a cwd for Node.js SBOM inputs that cdxgen needs in the + * default `pre-build` + `install-deps: false` mode. + */ +export type NodejsCdxgenSources = { + hasLockfile: boolean + hasNodeModules: boolean +} + +/** + * Probe upward from cwd for a recognized lockfile and for a co-located + * `node_modules/` directory. cdxgen's `pre-build` lifecycle needs at least one + * of these to produce a non-empty `components` array for a Node.js project. + */ +export async function detectNodejsCdxgenSources( + cwd: string = process.cwd(), +): Promise { + const [pnpmLockPath, npmLockPath, yarnLockPath, nodeModulesPath] = + await Promise.all([ + findUp(PNPM_LOCK_YAML, { cwd, onlyFiles: true }), + findUp(PACKAGE_LOCK_JSON, { cwd, onlyFiles: true }), + findUp(YARN_LOCK, { cwd, onlyFiles: true }), + findUp(NODE_MODULES, { cwd, onlyDirectories: true }), + ]) + return { + hasLockfile: Boolean(pnpmLockPath || npmLockPath || yarnLockPath), + hasNodeModules: Boolean(nodeModulesPath), + } +} + +/** + * True when the argv `type` resolves to a Node.js platform (the cdxgen default + * when the user does not pass `--type`). + */ +export function isNodejsCdxgenType(argvType: unknown): boolean { + if (argvType === undefined || argvType === null) { + return true + } + if (typeof argvType === 'string') { + return nodejsPlatformTypes.has(argvType) + } + if (Array.isArray(argvType)) { + return argvType.some( + t => typeof t === 'string' && nodejsPlatformTypes.has(t), + ) + } + return false +} + export type ArgvObject = { [key: string]: boolean | null | number | string | Array } @@ -122,7 +172,7 @@ export async function runCdxgen(argvObj: ArgvObject): Promise { }) // Use finally handler for cleanup instead of process.on('exit'). - cdxgenResult.spawnPromise.finally(() => { + cdxgenResult.spawnPromise.finally(async () => { if (cleanupPackageLock) { try { // This removes the temporary package-lock.json we created for cdxgen. @@ -150,9 +200,46 @@ export async function runCdxgen(argvObj: ArgvObject): Promise { } if (existsSync(fullOutputPath)) { logger.log(colors.cyanBright(`${outputPath} created!`)) + await warnIfEmptyComponents(fullOutputPath, argvMutable) } } }) return cdxgenResult } + +/** + * Read a generated CycloneDX BOM and warn when its `components` array is + * empty. An empty components array parses as valid CycloneDX but carries no + * dependency data, so the Socket dashboard cannot surface alerts for it. This + * catches configurations we did not hard-gate (non-default lifecycle, custom + * `--filter`/`--only` wiping all components, ecosystem mismatch, etc.). + */ +async function warnIfEmptyComponents( + outputPath: string, + argvMutable: ArgvObject, +): Promise { + let raw: string + try { + raw = await fs.readFile(outputPath, 'utf8') + } catch { + return + } + let bom: { components?: unknown } | undefined + try { + bom = JSON.parse(raw) + } catch { + return + } + if (!bom || !Array.isArray(bom.components) || bom.components.length > 0) { + return + } + const lifecycle = argvMutable['lifecycle'] + const lifecycleHint = + lifecycle === 'pre-build' || lifecycle === undefined + ? ' Pass --lifecycle build to resolve components during the build, or run a package install first so node_modules/ exists.\n' + : ' Re-check --type, --filter, and --only — a filter may be excluding every component.\n' + logger.warn( + `${outputPath} has an empty "components" array — the generated SBOM contains no dependencies and the Socket dashboard will show no alerts for it.\n${lifecycleHint}`, + ) +} diff --git a/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts b/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts index a77875a55..e08cc7f8c 100644 --- a/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts +++ b/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts @@ -37,8 +37,16 @@ vi.mock('@socketsecurity/lib/logger', () => ({ // Mock runCdxgen to prevent actual cdxgen execution. const mockRunCdxgen = vi.hoisted(() => vi.fn()) +const mockDetectNodejsCdxgenSources = vi.hoisted(() => + // Default to "sources available" so pre-existing tests don't trip the + // hard gate added in SMO-590. + vi.fn().mockResolvedValue({ hasLockfile: true, hasNodeModules: true }), +) +const mockIsNodejsCdxgenType = vi.hoisted(() => vi.fn().mockReturnValue(true)) vi.mock('../../../../src/commands/manifest/run-cdxgen.mts', () => ({ + detectNodejsCdxgenSources: mockDetectNodejsCdxgenSources, + isNodejsCdxgenType: mockIsNodejsCdxgenType, runCdxgen: mockRunCdxgen, })) @@ -49,6 +57,11 @@ const { cmdManifestCdxgen } = describe('cmd-manifest-cdxgen', () => { beforeEach(() => { vi.clearAllMocks() + mockDetectNodejsCdxgenSources.mockResolvedValue({ + hasLockfile: true, + hasNodeModules: true, + }) + mockIsNodejsCdxgenType.mockReturnValue(true) process.exitCode = undefined }) @@ -311,6 +324,98 @@ describe('cmd-manifest-cdxgen', () => { }) }) + describe('empty-components hard gate (SMO-590)', () => { + it('fails when default pre-build path has no lockfile and no node_modules', async () => { + mockDetectNodejsCdxgenSources.mockResolvedValue({ + hasLockfile: false, + hasNodeModules: false, + }) + + await cmdManifestCdxgen.run(['.'], importMeta, context) + + expect(process.exitCode).toBe(2) + expect(mockLogger.fail).toHaveBeenCalledWith( + expect.stringContaining('no lockfile'), + ) + expect(mockRunCdxgen).not.toHaveBeenCalled() + }) + + it('allows the run when only a lockfile is present', async () => { + mockDetectNodejsCdxgenSources.mockResolvedValue({ + hasLockfile: true, + hasNodeModules: false, + }) + const mockSpawnPromise = Promise.resolve({ code: 0, signal: null }) + mockRunCdxgen.mockResolvedValue({ spawnPromise: mockSpawnPromise }) + const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation((() => {}) as any) + + await cmdManifestCdxgen.run(['.'], importMeta, context) + + expect(mockRunCdxgen).toHaveBeenCalled() + mockExit.mockRestore() + }) + + it('allows the run when only node_modules/ is present', async () => { + mockDetectNodejsCdxgenSources.mockResolvedValue({ + hasLockfile: false, + hasNodeModules: true, + }) + const mockSpawnPromise = Promise.resolve({ code: 0, signal: null }) + mockRunCdxgen.mockResolvedValue({ spawnPromise: mockSpawnPromise }) + const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation((() => {}) as any) + + await cmdManifestCdxgen.run(['.'], importMeta, context) + + expect(mockRunCdxgen).toHaveBeenCalled() + mockExit.mockRestore() + }) + + it('skips the gate when user passes --lifecycle explicitly', async () => { + mockDetectNodejsCdxgenSources.mockResolvedValue({ + hasLockfile: false, + hasNodeModules: false, + }) + const mockSpawnPromise = Promise.resolve({ code: 0, signal: null }) + mockRunCdxgen.mockResolvedValue({ spawnPromise: mockSpawnPromise }) + const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation((() => {}) as any) + + await cmdManifestCdxgen.run( + ['--lifecycle', 'build', '.'], + importMeta, + context, + ) + + expect(mockDetectNodejsCdxgenSources).not.toHaveBeenCalled() + expect(mockRunCdxgen).toHaveBeenCalled() + mockExit.mockRestore() + }) + + it('skips the gate for non-Node.js project types', async () => { + mockIsNodejsCdxgenType.mockReturnValue(false) + const mockSpawnPromise = Promise.resolve({ code: 0, signal: null }) + mockRunCdxgen.mockResolvedValue({ spawnPromise: mockSpawnPromise }) + const mockExit = vi + .spyOn(process, 'exit') + .mockImplementation((() => {}) as any) + + await cmdManifestCdxgen.run( + ['--type', 'python', '.'], + importMeta, + context, + ) + + expect(mockDetectNodejsCdxgenSources).not.toHaveBeenCalled() + expect(mockRunCdxgen).toHaveBeenCalled() + mockExit.mockRestore() + }) + }) + describe('help flag handling', () => { it('should pass --help flag to cdxgen', async () => { const mockSpawnPromise = Promise.resolve({ code: 0, signal: null }) diff --git a/packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts b/packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts new file mode 100644 index 000000000..4fd5ddd79 --- /dev/null +++ b/packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts @@ -0,0 +1,90 @@ +/** + * Unit tests for run-cdxgen helpers. + * + * Covers the lockfile/node_modules probe and Node.js type detection that + * gate the default `socket cdxgen` path against shipping empty-components + * SBOMs (SMO-590). + */ + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const mockFindUp = vi.hoisted(() => vi.fn()) + +vi.mock('../../../../src/utils/fs/find-up.mts', () => ({ + findUp: mockFindUp, +})) + +const { detectNodejsCdxgenSources, isNodejsCdxgenType } = await import( + '../../../../src/commands/manifest/run-cdxgen.mts' +) + +describe('isNodejsCdxgenType', () => { + it('treats an undefined type as Node.js (the cdxgen default)', () => { + expect(isNodejsCdxgenType(undefined)).toBe(true) + expect(isNodejsCdxgenType(null)).toBe(true) + }) + + it.each(['js', 'javascript', 'typescript', 'nodejs', 'npm', 'pnpm', 'ts'])( + 'recognizes %s as Node.js', + type => { + expect(isNodejsCdxgenType(type)).toBe(true) + }, + ) + + it.each(['python', 'java', 'go', 'rust'])('rejects %s', type => { + expect(isNodejsCdxgenType(type)).toBe(false) + }) + + it('matches arrays containing at least one Node.js entry', () => { + expect(isNodejsCdxgenType(['python', 'js'])).toBe(true) + expect(isNodejsCdxgenType(['python', 'java'])).toBe(false) + }) +}) + +describe('detectNodejsCdxgenSources', () => { + beforeEach(() => { + mockFindUp.mockReset() + }) + + it('reports neither source when nothing is found', async () => { + mockFindUp.mockResolvedValue(undefined) + const result = await detectNodejsCdxgenSources('/tmp/project') + expect(result).toEqual({ hasLockfile: false, hasNodeModules: false }) + }) + + it('detects a pnpm-lock.yaml', async () => { + mockFindUp.mockImplementation((name: string) => + Promise.resolve(name === 'pnpm-lock.yaml' ? '/x/pnpm-lock.yaml' : undefined), + ) + const result = await detectNodejsCdxgenSources('/tmp/project') + expect(result.hasLockfile).toBe(true) + expect(result.hasNodeModules).toBe(false) + }) + + it('detects a package-lock.json', async () => { + mockFindUp.mockImplementation((name: string) => + Promise.resolve( + name === 'package-lock.json' ? '/x/package-lock.json' : undefined, + ), + ) + const result = await detectNodejsCdxgenSources('/tmp/project') + expect(result.hasLockfile).toBe(true) + }) + + it('detects a yarn.lock', async () => { + mockFindUp.mockImplementation((name: string) => + Promise.resolve(name === 'yarn.lock' ? '/x/yarn.lock' : undefined), + ) + const result = await detectNodejsCdxgenSources('/tmp/project') + expect(result.hasLockfile).toBe(true) + }) + + it('detects node_modules/', async () => { + mockFindUp.mockImplementation((name: string) => + Promise.resolve(name === 'node_modules' ? '/x/node_modules' : undefined), + ) + const result = await detectNodejsCdxgenSources('/tmp/project') + expect(result.hasLockfile).toBe(false) + expect(result.hasNodeModules).toBe(true) + }) +}) From 277662e5f5d3a64bac8df41335afb8f111153f79 Mon Sep 17 00:00:00 2001 From: jdalton Date: Thu, 23 Apr 2026 12:37:46 -0400 Subject: [PATCH 2/4] fix(cli): await the empty-BOM warning in cdxgen post-run chain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The soft gate attached an `async .finally()` handler to `spawnPromise` but `runCdxgen` returned the original `cdxgenResult` — not the chained promise. The caller in `cmd-manifest-cdxgen.mts` awaited the original `spawnPromise` and then called `process.exit(result.code)`, so when the async finally yielded at `await warnIfEmptyComponents(...)` the caller's continuation fired first and killed the process before the warning printed. Rebind `spawnPromise` on the returned result to the chained promise so `await spawnPromise` in the caller naturally awaits the cleanup + warning. .finally() preserves the original value and rejection, so semantics for existing callers are unchanged. TypeScript note: .finally() returns plain Promise which strips the `process` / `stdin` extras that SpawnResult carries. Callers only use the promise shape, so cast back to SpawnResult. --- .../cli/src/commands/manifest/run-cdxgen.mts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/commands/manifest/run-cdxgen.mts b/packages/cli/src/commands/manifest/run-cdxgen.mts index 7702d4d2b..2ced2f578 100644 --- a/packages/cli/src/commands/manifest/run-cdxgen.mts +++ b/packages/cli/src/commands/manifest/run-cdxgen.mts @@ -171,8 +171,11 @@ export async function runCdxgen(argvObj: ArgvObject): Promise { agent, }) - // Use finally handler for cleanup instead of process.on('exit'). - cdxgenResult.spawnPromise.finally(async () => { + // Post-run cleanup + empty-BOM warning. We replace spawnPromise with a + // chained promise so the caller's `await spawnPromise` also awaits this + // work — otherwise the caller's continuation (e.g. `process.exit`) races + // the first `await` inside the finally body and the warning never prints. + const chainedSpawnPromise = cdxgenResult.spawnPromise.finally(async () => { if (cleanupPackageLock) { try { // This removes the temporary package-lock.json we created for cdxgen. @@ -205,7 +208,14 @@ export async function runCdxgen(argvObj: ArgvObject): Promise { } }) - return cdxgenResult + // Cast back to SpawnResult: .finally() returns plain Promise which + // drops the `process` / `stdin` extras SpawnResult carries. Callers of + // runCdxgen only use `await spawnPromise` for the result, not those + // extras, so the shape loss is safe. + return { + ...cdxgenResult, + spawnPromise: chainedSpawnPromise as typeof cdxgenResult.spawnPromise, + } } /** From d45a4a99d75b1308ae8ca749fb9597914e6941a4 Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 24 Apr 2026 17:00:15 -0400 Subject: [PATCH 3/4] chore(cli): drop tracker ID references from cdxgen empty-components gate Tracker IDs belong in the commit/PR metadata, not in committed source. Replace the four inline mentions (one src comment, two test comments, one describe block label) with plain English that describes the behavior. --- packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts | 2 +- .../test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts | 4 ++-- packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts b/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts index c869f0d15..be5904a21 100644 --- a/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts +++ b/packages/cli/src/commands/manifest/cmd-manifest-cdxgen.mts @@ -308,7 +308,7 @@ async function run( // needs either a lockfile or an installed node_modules/ to produce any // Node.js components. Without both, it emits a valid CycloneDX doc with // "components": []. Refuse with an actionable error instead of shipping - // an empty SBOM (SMO-590). + // an empty SBOM. if ( lifecycleWasDefaulted && isNodejsCdxgenType(yargv.type) && diff --git a/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts b/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts index e08cc7f8c..2170d7844 100644 --- a/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts +++ b/packages/cli/test/unit/commands/manifest/cmd-manifest-cdxgen.test.mts @@ -39,7 +39,7 @@ vi.mock('@socketsecurity/lib/logger', () => ({ const mockRunCdxgen = vi.hoisted(() => vi.fn()) const mockDetectNodejsCdxgenSources = vi.hoisted(() => // Default to "sources available" so pre-existing tests don't trip the - // hard gate added in SMO-590. + // empty-components hard gate. vi.fn().mockResolvedValue({ hasLockfile: true, hasNodeModules: true }), ) const mockIsNodejsCdxgenType = vi.hoisted(() => vi.fn().mockReturnValue(true)) @@ -324,7 +324,7 @@ describe('cmd-manifest-cdxgen', () => { }) }) - describe('empty-components hard gate (SMO-590)', () => { + describe('empty-components hard gate', () => { it('fails when default pre-build path has no lockfile and no node_modules', async () => { mockDetectNodejsCdxgenSources.mockResolvedValue({ hasLockfile: false, diff --git a/packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts b/packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts index 4fd5ddd79..674b87f80 100644 --- a/packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts +++ b/packages/cli/test/unit/commands/manifest/run-cdxgen.test.mts @@ -3,7 +3,7 @@ * * Covers the lockfile/node_modules probe and Node.js type detection that * gate the default `socket cdxgen` path against shipping empty-components - * SBOMs (SMO-590). + * SBOMs. */ import { beforeEach, describe, expect, it, vi } from 'vitest' From 17aed0508cb36fdb19d6d25f3f4ca5841d73e26f Mon Sep 17 00:00:00 2001 From: jdalton Date: Fri, 24 Apr 2026 17:03:20 -0400 Subject: [PATCH 4/4] chore(deps): update pnpm-lock.yaml for new hook workspace members The merge from main pulled in two new workspace members under .claude/hooks/ (public-surface-reminder and token-hygiene) but did not refresh the lockfile for their declared dependencies, so CI rejected the branch with "1 dependencies were added: @types/node@24.9.2". Add the matching entries; no runtime code changes. --- pnpm-lock.yaml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 79a9d9cd6..ef25b5987 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -586,12 +586,27 @@ importers: specifier: 24.9.2 version: 24.9.2 + .claude/hooks/public-surface-reminder: + devDependencies: + '@types/node': + specifier: 24.9.2 + version: 24.9.2 + .claude/hooks/setup-security-tools: dependencies: '@socketsecurity/lib': specifier: 5.24.0 version: 5.24.0(typescript@5.9.3) + .claude/hooks/token-hygiene: + devDependencies: + '@socketsecurity/lib': + specifier: 5.24.0 + version: 5.24.0(typescript@5.9.3) + '@types/node': + specifier: 24.9.2 + version: 24.9.2 + packages/build-infra: dependencies: '@babel/parser':