From 2eff4c2490c5df3e89a40f2fa6bfb0e0a96b69ad Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Fri, 25 Sep 2026 22:08:28 +0200 Subject: [PATCH 1/6] feat(fix): add --dynamic-sbom-inference Generate Socket facts for every Gradle, sbt and Maven build root, upload them with the other manifests and have Coana attribute Maven artifacts only through them, so a fix lands only in the modules that resolve the vulnerable dependency. The generated files are restored after each PR-mode reset and removed once the fix is done. Facts files already present are still refused. PR mode now commits the files Coana reports writing, falling back to the uploaded manifest names, so build-script edits are no longer dropped. --- CHANGELOG.md | 8 + src/commands/fix/cmd-fix.integration.test.mts | 1 + src/commands/fix/cmd-fix.mts | 9 + .../coana-fix-dynamic-sbom-inference.test.mts | 241 ++++++++++++++++++ src/commands/fix/coana-fix.mts | 102 +++++++- src/commands/fix/generated-socket-facts.mts | 48 ++++ src/commands/fix/handle-fix.mts | 3 + src/commands/fix/types.mts | 1 + 8 files changed, 402 insertions(+), 11 deletions(-) create mode 100644 src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts create mode 100644 src/commands/fix/generated-socket-facts.mts diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aab296a2b..692e92191c 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/). +## [Unreleased] + +### Added +- `socket fix --dynamic-sbom-inference` generates Socket facts for each Gradle, sbt and Maven build and fixes a vulnerable dependency only in the projects/modules that resolve it. The generated files are removed afterwards. + +### Fixed +- Fixes opened as pull requests now include edits to build files that are not uploaded manifests, such as `gradle.properties` or sbt `project/*.scala` files. + ## [1.1.180](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.180) - 2026-09-25 ### Changed diff --git a/src/commands/fix/cmd-fix.integration.test.mts b/src/commands/fix/cmd-fix.integration.test.mts index 8b2908a4a0..0e364c3794 100644 --- a/src/commands/fix/cmd-fix.integration.test.mts +++ b/src/commands/fix/cmd-fix.integration.test.mts @@ -168,6 +168,7 @@ describe('socket fix', async () => { See GitHub documentation (https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-auto-merge-for-pull-requests-in-your-repository) for managing auto-merge for pull requests in your repository. --debug Enable debug logging in the Coana-based Socket Fix CLI invocation. --disable-external-tool-checks Disable external tool checks during fix analysis. + --dynamic-sbom-inference For Gradle, sbt, and Maven: generate a Socket facts SBOM (produced directly by each package manager) per independent build root, instead of one synthetic root. Fixes are then attributed to the projects/modules that actually resolve each vulnerable dependency. The generated files are removed afterwards. --ecosystems Limit fix analysis to specific ecosystems. Accepts space- or comma-separated values and is case-insensitive. Defaults to all ecosystems. --exclude-paths Skip matching paths from the scan entirely: manifests under these paths are not uploaded, and fixes are not applied to workspaces under them. Patterns are anchored micromatch globs matched relative to the target directory (CWD); \`data/postgres/pgdata\` matches that exact path, \`**/pgdata\` matches at any depth. Use this to skip directories the current user cannot read so they do not abort manifest collection. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags. --fix-version Override the version of @coana-tech/cli used for fix analysis. Default: . diff --git a/src/commands/fix/cmd-fix.mts b/src/commands/fix/cmd-fix.mts index 4ff010c046..684b8cde07 100644 --- a/src/commands/fix/cmd-fix.mts +++ b/src/commands/fix/cmd-fix.mts @@ -32,6 +32,7 @@ import { import { RangeStyles } from '../../utils/semver.mts' import { getDefaultOrgSlug } from '../ci/fetch-default-org-slug.mts' import { assertValidExcludePaths } from '../scan/exclude-paths.mts' +import { DYNAMIC_SBOM_INFERENCE_DESCRIPTION } from '../scan/reachability-flags.mts' import type { MeowFlag, MeowFlags } from '../../flags.mts' import type { PURL_Type } from '../../utils/ecosystem.mts' @@ -177,6 +178,11 @@ Available styles: default: false, description: 'Disable external tool checks during fix analysis.', }, + dynamicSbomInference: { + type: 'boolean', + default: false, + description: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} Fixes are then attributed to the projects/modules that actually resolve each vulnerable dependency. The generated files are removed afterwards.`, + }, ecosystems: { type: 'string', default: [], @@ -325,6 +331,7 @@ async function run( autopilot, debug, disableExternalToolChecks, + dynamicSbomInference, ecosystems, exclude, excludePaths, @@ -351,6 +358,7 @@ async function run( autopilot: boolean debug: boolean disableExternalToolChecks: boolean + dynamicSbomInference: boolean ecosystems: string[] exclude: string[] excludePaths: string[] @@ -519,6 +527,7 @@ async function run( debug, disableExternalToolChecks, disableMajorUpdates, + dynamicSbomInference, ecosystems: validatedEcosystems, exclude: excludePatterns, excludePaths: excludePathsPatterns, diff --git a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts new file mode 100644 index 0000000000..283b5bebec --- /dev/null +++ b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts @@ -0,0 +1,241 @@ +import { promises as fs } from 'node:fs' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { logger } from '@socketsecurity/registry/lib/logger' + +import { coanaFix } from './coana-fix.mts' + +import type { FixConfig } from './types.mts' + +// Mock all external dependencies. +const mockSpawnCoanaDlx = vi.hoisted(() => vi.fn()) +const mockSetupSdk = vi.hoisted(() => vi.fn()) +const mockFetchSupportedScanFileNames = vi.hoisted(() => vi.fn()) +const mockGetPackageFilesForScan = vi.hoisted(() => vi.fn()) +const mockHandleApiCall = vi.hoisted(() => vi.fn()) +const mockGetFixEnv = vi.hoisted(() => vi.fn()) +const mockGetSocketFixPrs = vi.hoisted(() => vi.fn()) +const mockFetchGhsaDetails = vi.hoisted(() => vi.fn()) +const mockGitUnstagedModifiedFiles = vi.hoisted(() => vi.fn()) +const mockGitCommit = vi.hoisted(() => vi.fn()) +const mockGenerateSocketFactsForFix = vi.hoisted(() => vi.fn()) + +vi.mock('../../utils/dlx.mts', () => ({ + spawnCoanaDlx: mockSpawnCoanaDlx, +})) + +vi.mock('../../utils/sdk.mts', () => ({ + setupSdk: mockSetupSdk, +})) + +vi.mock('../scan/fetch-supported-scan-file-names.mts', () => ({ + fetchSupportedScanFileNames: mockFetchSupportedScanFileNames, +})) + +vi.mock('../../utils/path-resolve.mts', () => ({ + getPackageFilesForScan: mockGetPackageFilesForScan, +})) + +vi.mock('../../utils/api.mts', () => ({ + handleApiCall: mockHandleApiCall, +})) + +vi.mock('./env-helpers.mts', () => ({ + checkCiEnvVars: vi.fn(() => ({ missing: [], present: [] })), + getCiEnvInstructions: vi.fn(() => 'Set CI env vars'), + getFixEnv: mockGetFixEnv, +})) + +vi.mock('./pull-request.mts', () => ({ + getSocketFixPrs: mockGetSocketFixPrs, + openSocketFixPr: vi.fn(), +})) + +vi.mock('../../utils/github.mts', () => ({ + enablePrAutoMerge: vi.fn(), + fetchGhsaDetails: mockFetchGhsaDetails, + setGitRemoteGithubRepoUrl: vi.fn(), +})) + +vi.mock('../../utils/git.mts', () => ({ + gitCheckoutBranch: vi.fn(() => Promise.resolve(true)), + gitCommit: mockGitCommit, + gitCreateBranch: vi.fn(() => Promise.resolve(true)), + gitDeleteBranch: vi.fn(() => Promise.resolve(true)), + gitPushBranch: vi.fn(() => Promise.resolve(true)), + gitRemoteBranchExists: vi.fn(() => Promise.resolve(false)), + gitResetAndClean: vi.fn(() => Promise.resolve(true)), + gitUnstagedModifiedFiles: mockGitUnstagedModifiedFiles, +})) + +vi.mock('./generated-socket-facts.mts', () => ({ + generateSocketFactsForFix: mockGenerateSocketFactsForFix, +})) + +vi.mock('./branch-cleanup.mts', () => ({ + cleanupErrorBranches: vi.fn(), + cleanupFailedPrBranches: vi.fn(), + cleanupStaleBranch: vi.fn(() => Promise.resolve(true)), + cleanupSuccessfulPrLocalBranch: vi.fn(), +})) + +const FACTS = '/test/cwd/app/.socket.facts.json' + +function coanaCalls(command: string): string[][] { + return mockSpawnCoanaDlx.mock.calls + .map(call => call[0] as string[]) + .filter(args => args[0] === command) +} + +describe('socket fix --dynamic-sbom-inference', () => { + const baseConfig: FixConfig = { + all: false, + applyFixes: true, + autopilot: false, + coanaVersion: undefined, + cwd: '/test/cwd', + debug: false, + disableExternalToolChecks: false, + disableMajorUpdates: false, + dynamicSbomInference: true, + ecosystems: [], + exclude: [], + excludePaths: [], + ghsas: ['GHSA-1111-1111-1111', 'GHSA-2222-2222-2222'], + include: [], + minSatisfying: false, + minimumReleaseAge: '', + orgSlug: 'test-org', + outputFile: '', + packageManagers: [], + prCheck: true, + prLimit: 10, + rangeStyle: 'preserve', + showAffectedDirectDependencies: false, + silence: true, + spinner: undefined, + unknownFlags: [], + } + const uploadManifestFiles = vi.fn() + const generated = { + paths: [FACTS], + remove: vi.fn(), + restore: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + mockSetupSdk.mockResolvedValue({ ok: true, data: { uploadManifestFiles } }) + mockFetchSupportedScanFileNames.mockResolvedValue({ ok: true, data: {} }) + mockGetPackageFilesForScan.mockResolvedValue(['/test/cwd/app/build.gradle']) + mockHandleApiCall.mockResolvedValue({ ok: true, data: { tarHash: 'hash' } }) + mockGenerateSocketFactsForFix.mockResolvedValue(generated) + mockGetFixEnv.mockResolvedValue({ isCi: false, repoInfo: null }) + mockGitUnstagedModifiedFiles.mockResolvedValue({ ok: true, data: [] }) + mockGitCommit.mockResolvedValue(true) + mockSpawnCoanaDlx.mockResolvedValue({ ok: true, data: '' }) + }) + + it('uploads the generated facts, restricts Maven artifacts to them and removes them', async () => { + const result = await coanaFix(baseConfig) + + expect(result.ok).toBe(true) + expect(uploadManifestFiles).toHaveBeenCalledWith( + 'test-org', + ['/test/cwd/app/build.gradle', FACTS], + { pathsRelativeTo: '/test/cwd' }, + ) + expect(coanaCalls('compute-fixes-and-upgrade-purls')[0]).toContain( + '--maven-use-only-socket-facts', + ) + expect(generated.remove).toHaveBeenCalledTimes(1) + }) + + it('discovers vulnerabilities only through the generated facts', async () => { + mockSpawnCoanaDlx.mockImplementation(async (args: string[]) => { + if (args[0] === 'find-vulnerabilities') { + await fs.writeFile( + args[args.indexOf('--output-file') + 1]!, + JSON.stringify({ ghsaIds: [], artifactCount: 1 }), + ) + } + return { ok: true, data: '' } + }) + + await coanaFix({ ...baseConfig, all: true, ghsas: [] }) + + expect(coanaCalls('find-vulnerabilities')[0]).toContain( + '--maven-use-only-socket-facts', + ) + }) + + it('still refuses facts files that were already present', async () => { + mockGetPackageFilesForScan.mockResolvedValue([ + '/test/cwd/app/.socket.facts.json', + ]) + + const result = await coanaFix(baseConfig) + + expect(result.ok).toBe(false) + expect(mockGenerateSocketFactsForFix).not.toHaveBeenCalled() + }) + + it('does not pass the facts restriction without the flag', async () => { + await coanaFix({ ...baseConfig, dynamicSbomInference: false }) + + expect(mockGenerateSocketFactsForFix).not.toHaveBeenCalled() + expect(coanaCalls('compute-fixes-and-upgrade-purls')[0]).not.toContain( + '--maven-use-only-socket-facts', + ) + }) + + describe('in PR mode', () => { + beforeEach(() => { + mockGetFixEnv.mockResolvedValue({ + baseBranch: 'main', + githubToken: 'test-token', + gitEmail: 'test@example.com', + gitUser: 'test-user', + isCi: true, + repoInfo: { defaultBranch: 'main', owner: 'o', repo: 'r' }, + }) + mockGetSocketFixPrs.mockResolvedValue([]) + mockFetchGhsaDetails.mockResolvedValue(new Map()) + }) + + it('restores the facts before every fix, since resetting cleans them away', async () => { + await coanaFix(baseConfig) + + expect(coanaCalls('compute-fixes-and-upgrade-purls')).toHaveLength(2) + expect(generated.restore).toHaveBeenCalledTimes(2) + expect(generated.remove).toHaveBeenCalledTimes(1) + }) + + it('commits the files the fix reports writing', async () => { + mockSpawnCoanaDlx.mockImplementation(async (args: string[]) => { + await fs.writeFile( + args[args.indexOf('--output-file') + 1]!, + JSON.stringify({ + type: 'applied-fixes', + fixes: {}, + modifiedFiles: ['app/build.gradle', 'gradle/versions.gradle'], + }), + ) + return { ok: true, data: '' } + }) + mockGitUnstagedModifiedFiles.mockResolvedValue({ + ok: true, + data: ['app/build.gradle', 'gradle/versions.gradle', 'README.md'], + }) + + await coanaFix({ ...baseConfig, ghsas: ['GHSA-1111-1111-1111'] }) + + expect(mockGitCommit).toHaveBeenCalledWith( + expect.any(String), + ['app/build.gradle', 'gradle/versions.gradle'], + expect.anything(), + ) + }) + }) +}) diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index c43b6baaa3..7f6f91494f 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -19,6 +19,7 @@ import { getCiEnvInstructions, getFixEnv, } from './env-helpers.mts' +import { generateSocketFactsForFix } from './generated-socket-facts.mts' import { getSocketFixBranchName, getSocketFixCommitMessage } from './git.mts' import { getSocketFixPrs, openSocketFixPr } from './pull-request.mts' import { @@ -30,6 +31,7 @@ import { handleApiCall } from '../../utils/api.mts' import { findSocketYmlSync } from '../../utils/config.mts' import { spawnCoanaDlx } from '../../utils/dlx.mts' import { getErrorCause } from '../../utils/errors.mts' +import { withTmpDir } from '../../utils/fs.mts' import { gitCheckoutBranch, gitCommit, @@ -50,6 +52,7 @@ import { setupSdk } from '../../utils/sdk.mts' import { excludePathToScanIgnores } from '../scan/exclude-paths.mts' import { fetchSupportedScanFileNames } from '../scan/fetch-supported-scan-file-names.mts' +import type { GeneratedSocketFacts } from './generated-socket-facts.mts' import type { FixConfig } from './types.mts' import type { CResult } from '../../types.mts' import type { PURL_Type } from '../../utils/ecosystem.mts' @@ -59,6 +62,7 @@ type DiscoverGhsaIdsOptions = { coanaVersion?: string | undefined cwd?: string | undefined ecosystems?: PURL_Type[] | undefined + factsOnly?: boolean | undefined packageManagers?: string[] | undefined silence?: boolean | undefined spinner?: Spinner | undefined @@ -137,6 +141,7 @@ async function discoverGhsaIds( const { cwd = process.cwd(), ecosystems, + factsOnly = false, packageManagers, silence = false, spinner, @@ -165,6 +170,7 @@ async function discoverGhsaIds( ...(packageManagers?.length ? ['--package-managers', ...packageManagers] : []), + ...(factsOnly ? ['--maven-use-only-socket-facts'] : []), ], orgSlug, { @@ -187,9 +193,46 @@ async function discoverGhsaIds( } } -export async function coanaFix( +function isFactsFile(filepath: string): boolean { + return path.basename(filepath).toLowerCase() === DOT_SOCKET_DOT_FACTS_JSON +} + +function readWrittenFiles(outputFile: string): Set | undefined { + const result = readJsonSync(outputFile, { throws: false }) as + | { modifiedFiles?: unknown } + | null + | undefined + const files = result?.modifiedFiles + return Array.isArray(files) && files.every(f => typeof f === 'string') + ? new Set(files) + : undefined +} + +type CoanaFixResult = CResult<{ fixedAll: boolean; ghsaDetails: unknown[] }> + +type GeneratedSocketFactsSlot = { + generated?: GeneratedSocketFacts | undefined + tmpDir: string +} + +export async function coanaFix(fixConfig: FixConfig): Promise { + if (!fixConfig.dynamicSbomInference) { + return await coanaFixWithFacts(fixConfig, undefined) + } + return await withTmpDir('socket-fix-facts-', async tmpDir => { + const slot: GeneratedSocketFactsSlot = { tmpDir } + try { + return await coanaFixWithFacts(fixConfig, slot) + } finally { + await slot.generated?.remove() + } + }) +} + +async function coanaFixWithFacts( fixConfig: FixConfig, -): Promise> { + factsSlot: GeneratedSocketFactsSlot | undefined, +): Promise { const { all, applyFixes, @@ -258,16 +301,16 @@ export async function coanaFix( // sibling manifest's references). --exclude stays separate as a hidden // legacy escape hatch for the narrower "fix-application only" semantic. const coanaExcludePatterns = [...exclude, ...excludePaths] - const scanFilepaths = await getPackageFilesForScan(['.'], supportedFiles, { - additionalIgnores, - config: socketConfig, - cwd, - }) + const findScanFilepaths = () => + getPackageFilesForScan(['.'], supportedFiles, { + additionalIgnores, + config: socketConfig, + cwd, + }) + const scanFilepaths = await findScanFilepaths() // Fail if any .socket.facts.json files are present in the scan folder. // These are analysis artifacts and must be removed before re-running fix. - const factsFiles = scanFilepaths.filter( - p => path.basename(p).toLowerCase() === DOT_SOCKET_DOT_FACTS_JSON, - ) + const factsFiles = scanFilepaths.filter(isFactsFile) if (factsFiles.length) { if (!silence) { spinner?.stop() @@ -280,6 +323,31 @@ export async function coanaFix( factsFiles.map(p => ` - ${p}`).join('\n'), } } + if (factsSlot) { + if (!silence) { + spinner?.stop() + logger.info( + 'Generating Socket facts for Gradle, sbt and Maven builds ...', + ) + } + try { + factsSlot.generated = await generateSocketFactsForFix({ + cwd, + excludePaths, + tmpDir: factsSlot.tmpDir, + }) + } catch (e) { + // A failed build root aborts inference after others wrote their facts. + const partial = (await findScanFilepaths()).filter(isFactsFile) + await Promise.all(partial.map(p => fs.rm(p, { force: true }))) + throw e + } + scanFilepaths.push(...factsSlot.generated.paths) + if (!silence) { + spinner?.start() + } + } + const factsOnlyFlags = factsSlot ? ['--maven-use-only-socket-facts'] : [] const uploadCResult = await handleApiCall( sockSdk.uploadManifestFiles(orgSlug, scanFilepaths, { pathsRelativeTo: cwd, @@ -347,6 +415,7 @@ export async function coanaFix( coanaVersion, cwd, ecosystems, + factsOnly: !!factsSlot, packageManagers, silence, spinner, @@ -396,6 +465,7 @@ export async function coanaFix( ...(packageManagers.length ? ['--package-managers', ...packageManagers] : []), + ...factsOnlyFlags, ...(!applyFixes ? [FLAG_DRY_RUN] : []), '--output-file', tmpFile, @@ -492,6 +562,7 @@ export async function coanaFix( coanaVersion, cwd, ecosystems, + factsOnly: !!factsSlot, packageManagers, silence, spinner, @@ -539,6 +610,10 @@ export async function coanaFix( const ghsaId = ids[i]! debugFn('notice', `check: ${ghsaId}`) + // Resetting to the base branch cleans the untracked facts files away. + // eslint-disable-next-line no-await-in-loop + await factsSlot?.generated?.restore() + // Create a temporary file for Coana output. const tmpDir = os.tmpdir() const tmpFile = path.join(tmpDir, `socket-fix-${ghsaId}-${Date.now()}.json`) @@ -567,6 +642,7 @@ export async function coanaFix( ...(packageManagers.length ? ['--package-managers', ...packageManagers] : []), + ...factsOnlyFlags, ...(debug ? ['--debug'] : []), ...(disableExternalToolChecks ? ['--disable-external-tool-checks'] @@ -607,9 +683,13 @@ export async function coanaFix( // Check for modified files after applying the fix. // eslint-disable-next-line no-await-in-loop const unstagedCResult = await gitUnstagedModifiedFiles(cwd) + // Build scripts the fix edits need not be manifests the scan uploads. + const writtenFiles = readWrittenFiles(tmpFile) const modifiedFiles = unstagedCResult.ok ? unstagedCResult.data.filter(relPath => - scanBaseNames.has(path.basename(relPath)), + writtenFiles + ? writtenFiles.has(relPath) + : scanBaseNames.has(path.basename(relPath)), ) : [] diff --git a/src/commands/fix/generated-socket-facts.mts b/src/commands/fix/generated-socket-facts.mts new file mode 100644 index 0000000000..7e9afa4627 --- /dev/null +++ b/src/commands/fix/generated-socket-facts.mts @@ -0,0 +1,48 @@ +import { copyFile, rm } from 'node:fs/promises' +import path from 'node:path' + +import { runDynamicSbomInference } from '../scan/run-dynamic-sbom-inference.mts' + +export type GeneratedSocketFacts = { + paths: string[] + remove: () => Promise + restore: () => Promise +} + +// The generated files describe the build before any fix, so they are kept +// aside for restoring after `git clean` and removed once the fix is done. +export async function generateSocketFactsForFix({ + cwd, + excludePaths, + tmpDir, +}: { + cwd: string + excludePaths: string[] + tmpDir: string +}): Promise { + const { factsPaths } = await runDynamicSbomInference({ + cwd, + excludePaths, + sbtTmpDir: undefined, + withFiles: false, + }) + const paths = factsPaths.map(p => path.resolve(cwd, p)) + const backups = await Promise.all( + paths.map(async (source, index) => { + const backup = path.join(tmpDir, `${index}.json`) + await copyFile(source, backup) + return { backup, source } + }), + ) + return { + paths, + async remove() { + await Promise.all(paths.map(p => rm(p, { force: true }))) + }, + async restore() { + await Promise.all( + backups.map(({ backup, source }) => copyFile(backup, source)), + ) + }, + } +} diff --git a/src/commands/fix/handle-fix.mts b/src/commands/fix/handle-fix.mts index fb37fd5d98..dbf83ba9bb 100644 --- a/src/commands/fix/handle-fix.mts +++ b/src/commands/fix/handle-fix.mts @@ -122,6 +122,7 @@ export async function handleFix({ debug, disableExternalToolChecks, disableMajorUpdates, + dynamicSbomInference, ecosystems, exclude, excludePaths, @@ -151,6 +152,7 @@ export async function handleFix({ debug, disableExternalToolChecks, disableMajorUpdates, + dynamicSbomInference, ecosystems, exclude, excludePaths, @@ -179,6 +181,7 @@ export async function handleFix({ debug, disableExternalToolChecks, disableMajorUpdates, + dynamicSbomInference, ecosystems, exclude, excludePaths, diff --git a/src/commands/fix/types.mts b/src/commands/fix/types.mts index 3a436a71fd..a14500fc8c 100644 --- a/src/commands/fix/types.mts +++ b/src/commands/fix/types.mts @@ -11,6 +11,7 @@ export type FixConfig = { debug: boolean disableExternalToolChecks: boolean disableMajorUpdates: boolean + dynamicSbomInference: boolean ecosystems: PURL_Type[] exclude: string[] excludePaths: string[] From 28562b328f709c2072ac7c02fe2a263c88939dee Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Sat, 26 Sep 2026 00:22:28 +0200 Subject: [PATCH 2/6] fix(fix): commit the files a fix creates in PR mode Untracked files the fix reports writing, such as new sbt override files, were left out of the commit because only git's tracked changes were considered. --- CHANGELOG.md | 2 +- .../coana-fix-dynamic-sbom-inference.test.mts | 40 +++++++++++++++++++ src/commands/fix/coana-fix.mts | 25 ++++++++---- src/commands/fix/handle-fix-limit.test.mts | 4 ++ src/utils/git.mts | 27 +++++++++++++ 5 files changed, 89 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 692e92191c..71745bf08e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - `socket fix --dynamic-sbom-inference` generates Socket facts for each Gradle, sbt and Maven build and fixes a vulnerable dependency only in the projects/modules that resolve it. The generated files are removed afterwards. ### Fixed -- Fixes opened as pull requests now include edits to build files that are not uploaded manifests, such as `gradle.properties` or sbt `project/*.scala` files. +- Fixes opened as pull requests now include edits to build files that are not uploaded manifests, such as `gradle.properties` or sbt `project/*.scala` files, and the files a fix creates. ## [1.1.180](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.180) - 2026-09-25 diff --git a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts index 283b5bebec..c2bfd90152 100644 --- a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts +++ b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts @@ -18,6 +18,9 @@ const mockGetFixEnv = vi.hoisted(() => vi.fn()) const mockGetSocketFixPrs = vi.hoisted(() => vi.fn()) const mockFetchGhsaDetails = vi.hoisted(() => vi.fn()) const mockGitUnstagedModifiedFiles = vi.hoisted(() => vi.fn()) +const mockGitUntrackedFiles = vi.hoisted(() => + vi.fn(async () => ({ ok: true, data: [] })), +) const mockGitCommit = vi.hoisted(() => vi.fn()) const mockGenerateSocketFactsForFix = vi.hoisted(() => vi.fn()) @@ -67,6 +70,7 @@ vi.mock('../../utils/git.mts', () => ({ gitRemoteBranchExists: vi.fn(() => Promise.resolve(false)), gitResetAndClean: vi.fn(() => Promise.resolve(true)), gitUnstagedModifiedFiles: mockGitUnstagedModifiedFiles, + gitUntrackedFiles: mockGitUntrackedFiles, })) vi.mock('./generated-socket-facts.mts', () => ({ @@ -237,5 +241,41 @@ describe('socket fix --dynamic-sbom-inference', () => { expect.anything(), ) }) + + it('commits files the fix creates', async () => { + mockSpawnCoanaDlx.mockImplementation(async (args: string[]) => { + await fs.writeFile( + args[args.indexOf('--output-file') + 1]!, + JSON.stringify({ + type: 'applied-fixes', + fixes: {}, + modifiedFiles: [ + 'build.sbt', + 'project/SocketDependencyOverrides.scala', + ], + }), + ) + return { ok: true, data: '' } + }) + mockGitUnstagedModifiedFiles.mockResolvedValue({ + ok: true, + data: ['build.sbt'], + }) + mockGitUntrackedFiles.mockResolvedValue({ + ok: true, + data: [ + 'project/SocketDependencyOverrides.scala', + 'app/.socket.facts.json', + ], + }) + + await coanaFix({ ...baseConfig, ghsas: ['GHSA-1111-1111-1111'] }) + + expect(mockGitCommit).toHaveBeenCalledWith( + expect.any(String), + ['build.sbt', 'project/SocketDependencyOverrides.scala'], + expect.anything(), + ) + }) }) }) diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index 7f6f91494f..af9fdbe54f 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -41,6 +41,7 @@ import { gitRemoteBranchExists, gitResetAndClean, gitUnstagedModifiedFiles, + gitUntrackedFiles, } from '../../utils/git.mts' import { enablePrAutoMerge, @@ -683,15 +684,23 @@ async function coanaFixWithFacts( // Check for modified files after applying the fix. // eslint-disable-next-line no-await-in-loop const unstagedCResult = await gitUnstagedModifiedFiles(cwd) - // Build scripts the fix edits need not be manifests the scan uploads. + // Build scripts the fix edits need not be manifests the scan uploads, + // and files it creates are untracked. const writtenFiles = readWrittenFiles(tmpFile) - const modifiedFiles = unstagedCResult.ok - ? unstagedCResult.data.filter(relPath => - writtenFiles - ? writtenFiles.has(relPath) - : scanBaseNames.has(path.basename(relPath)), - ) - : [] + // eslint-disable-next-line no-await-in-loop + const untrackedCResult = writtenFiles + ? await gitUntrackedFiles(cwd) + : undefined + const modifiedFiles = writtenFiles + ? [ + ...(unstagedCResult.ok ? unstagedCResult.data : []), + ...(untrackedCResult?.ok ? untrackedCResult.data : []), + ].filter(relPath => writtenFiles.has(relPath)) + : unstagedCResult.ok + ? unstagedCResult.data.filter(relPath => + scanBaseNames.has(path.basename(relPath)), + ) + : [] if (!modifiedFiles.length) { debugFn('notice', `skip: no changes for ${ghsaId}`) diff --git a/src/commands/fix/handle-fix-limit.test.mts b/src/commands/fix/handle-fix-limit.test.mts index e6ad6af39a..bf83ecf213 100644 --- a/src/commands/fix/handle-fix-limit.test.mts +++ b/src/commands/fix/handle-fix-limit.test.mts @@ -18,6 +18,9 @@ const mockGetFixEnv = vi.hoisted(() => vi.fn()) const mockGetSocketFixPrs = vi.hoisted(() => vi.fn()) const mockFetchGhsaDetails = vi.hoisted(() => vi.fn()) const mockGitUnstagedModifiedFiles = vi.hoisted(() => vi.fn()) +const mockGitUntrackedFiles = vi.hoisted(() => + vi.fn(async () => ({ ok: true, data: [] })), +) vi.mock('../../utils/dlx.mts', () => ({ spawnCoanaDlx: mockSpawnCoanaDlx, @@ -65,6 +68,7 @@ vi.mock('../../utils/git.mts', () => ({ gitRemoteBranchExists: vi.fn(() => Promise.resolve(false)), gitResetAndClean: vi.fn(() => Promise.resolve(true)), gitUnstagedModifiedFiles: mockGitUnstagedModifiedFiles, + gitUntrackedFiles: mockGitUntrackedFiles, })) vi.mock('./branch-cleanup.mts', () => ({ diff --git a/src/utils/git.mts b/src/utils/git.mts index eda0efd265..4d804170dd 100644 --- a/src/utils/git.mts +++ b/src/utils/git.mts @@ -533,6 +533,33 @@ export async function gitUnstagedModifiedFiles( } } +export async function gitUntrackedFiles( + cwd = process.cwd(), +): Promise> { + try { + const result = await spawn( + 'git', + ['ls-files', '--others', '--exclude-standard'], + { cwd }, + ) + return { + ok: true, + data: result.stdout + .split('\n') + .filter(Boolean) + .map(p => normalizePath(p)), + } + } catch (e) { + debugFn('error', 'Failed to list untracked files') + debugDir('error', e) + return { + ok: false, + message: 'Git Error', + cause: 'Unexpected error while trying to list untracked files', + } + } +} + const parsedGitRemoteUrlCache = new Map() export function parseGitRemoteUrl(remoteUrl: string): RepoInfo | undefined { From a66a450c0c864d580913b35eda4d9a3740ecb421 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Sat, 26 Sep 2026 06:29:18 +0200 Subject: [PATCH 3/6] feat(fix): pass the facts files' classpaths to Coana The facts files merge components by version across a build, so they over-approximate which projects resolve a dependency. socket fix --dynamic-sbom-inference now keeps the sidecar's per-project classpaths, without resolving artifact paths, and passes it to compute-fixes-and-upgrade-purls. --- .../coana-fix-dynamic-sbom-inference.test.mts | 7 +++- src/commands/fix/coana-fix.mts | 29 +++++++------ src/commands/fix/generated-socket-facts.mts | 16 ++++++- src/commands/manifest/run-manifest-facts.mts | 3 +- src/commands/manifest/scripts/sidecar.mts | 10 +++-- .../manifest/scripts/sidecar.test.mts | 42 +++++++++++++++++++ .../scan/run-dynamic-sbom-inference.mts | 9 ++-- 7 files changed, 93 insertions(+), 23 deletions(-) diff --git a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts index c2bfd90152..6a88d87196 100644 --- a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts +++ b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts @@ -124,6 +124,7 @@ describe('socket fix --dynamic-sbom-inference', () => { const uploadManifestFiles = vi.fn() const generated = { paths: [FACTS], + sidecarFile: '/tmp/socket-fix-facts/sidecar.json', remove: vi.fn(), restore: vi.fn(), } @@ -150,8 +151,10 @@ describe('socket fix --dynamic-sbom-inference', () => { ['/test/cwd/app/build.gradle', FACTS], { pathsRelativeTo: '/test/cwd' }, ) - expect(coanaCalls('compute-fixes-and-upgrade-purls')[0]).toContain( - '--maven-use-only-socket-facts', + const args = coanaCalls('compute-fixes-and-upgrade-purls')[0]! + expect(args).toContain('--maven-use-only-socket-facts') + expect(args[args.indexOf('--compute-artifacts-sidecar') + 1]).toBe( + generated.sidecarFile, ) expect(generated.remove).toHaveBeenCalledTimes(1) }) diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index af9fdbe54f..6a7d17c3cd 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -63,7 +63,7 @@ type DiscoverGhsaIdsOptions = { coanaVersion?: string | undefined cwd?: string | undefined ecosystems?: PURL_Type[] | undefined - factsOnly?: boolean | undefined + factsFlags?: string[] | undefined packageManagers?: string[] | undefined silence?: boolean | undefined spinner?: Spinner | undefined @@ -142,7 +142,7 @@ async function discoverGhsaIds( const { cwd = process.cwd(), ecosystems, - factsOnly = false, + factsFlags = [], packageManagers, silence = false, spinner, @@ -171,7 +171,7 @@ async function discoverGhsaIds( ...(packageManagers?.length ? ['--package-managers', ...packageManagers] : []), - ...(factsOnly ? ['--maven-use-only-socket-facts'] : []), + ...factsFlags, ], orgSlug, { @@ -348,7 +348,14 @@ async function coanaFixWithFacts( spinner?.start() } } - const factsOnlyFlags = factsSlot ? ['--maven-use-only-socket-facts'] : [] + const sidecarFile = factsSlot?.generated?.sidecarFile + // Discovery only needs which artifacts the facts files resolve; applying + // fixes also needs each project's exact classpath from the sidecar. + const discoveryFlags = factsSlot ? ['--maven-use-only-socket-facts'] : [] + const factsFlags = [ + ...discoveryFlags, + ...(sidecarFile ? ['--compute-artifacts-sidecar', sidecarFile] : []), + ] const uploadCResult = await handleApiCall( sockSdk.uploadManifestFiles(orgSlug, scanFilepaths, { pathsRelativeTo: cwd, @@ -416,7 +423,7 @@ async function coanaFixWithFacts( coanaVersion, cwd, ecosystems, - factsOnly: !!factsSlot, + factsFlags: discoveryFlags, packageManagers, silence, spinner, @@ -466,7 +473,7 @@ async function coanaFixWithFacts( ...(packageManagers.length ? ['--package-managers', ...packageManagers] : []), - ...factsOnlyFlags, + ...factsFlags, ...(!applyFixes ? [FLAG_DRY_RUN] : []), '--output-file', tmpFile, @@ -563,7 +570,7 @@ async function coanaFixWithFacts( coanaVersion, cwd, ecosystems, - factsOnly: !!factsSlot, + factsFlags: discoveryFlags, packageManagers, silence, spinner, @@ -643,7 +650,7 @@ async function coanaFixWithFacts( ...(packageManagers.length ? ['--package-managers', ...packageManagers] : []), - ...factsOnlyFlags, + ...factsFlags, ...(debug ? ['--debug'] : []), ...(disableExternalToolChecks ? ['--disable-external-tool-checks'] @@ -688,13 +695,11 @@ async function coanaFixWithFacts( // and files it creates are untracked. const writtenFiles = readWrittenFiles(tmpFile) // eslint-disable-next-line no-await-in-loop - const untrackedCResult = writtenFiles - ? await gitUntrackedFiles(cwd) - : undefined + const untrackedCResult = await gitUntrackedFiles(cwd) const modifiedFiles = writtenFiles ? [ ...(unstagedCResult.ok ? unstagedCResult.data : []), - ...(untrackedCResult?.ok ? untrackedCResult.data : []), + ...(untrackedCResult.ok ? untrackedCResult.data : []), ].filter(relPath => writtenFiles.has(relPath)) : unstagedCResult.ok ? unstagedCResult.data.filter(relPath => diff --git a/src/commands/fix/generated-socket-facts.mts b/src/commands/fix/generated-socket-facts.mts index 7e9afa4627..15d4bb2bda 100644 --- a/src/commands/fix/generated-socket-facts.mts +++ b/src/commands/fix/generated-socket-facts.mts @@ -1,16 +1,20 @@ -import { copyFile, rm } from 'node:fs/promises' +import { copyFile, rm, writeFile } from 'node:fs/promises' import path from 'node:path' import { runDynamicSbomInference } from '../scan/run-dynamic-sbom-inference.mts' export type GeneratedSocketFacts = { paths: string[] + // The facts files' per-project classpaths, outside the repository. + sidecarFile: string | undefined remove: () => Promise restore: () => Promise } // The generated files describe the build before any fix, so they are kept // aside for restoring after `git clean` and removed once the fix is done. +// Their sidecar gives each project's exact classpath, which the facts +// files' merged component graph over-approximates. export async function generateSocketFactsForFix({ cwd, excludePaths, @@ -20,12 +24,19 @@ export async function generateSocketFactsForFix({ excludePaths: string[] tmpDir: string }): Promise { - const { factsPaths } = await runDynamicSbomInference({ + const { factsPaths, resolvedPathsSidecar } = await runDynamicSbomInference({ cwd, excludePaths, sbtTmpDir: undefined, + sidecar: true, withFiles: false, }) + const sidecarFile = resolvedPathsSidecar + ? path.join(tmpDir, 'sidecar.json') + : undefined + if (sidecarFile) { + await writeFile(sidecarFile, JSON.stringify(resolvedPathsSidecar)) + } const paths = factsPaths.map(p => path.resolve(cwd, p)) const backups = await Promise.all( paths.map(async (source, index) => { @@ -36,6 +47,7 @@ export async function generateSocketFactsForFix({ ) return { paths, + sidecarFile, async remove() { await Promise.all(paths.map(p => rm(p, { force: true }))) }, diff --git a/src/commands/manifest/run-manifest-facts.mts b/src/commands/manifest/run-manifest-facts.mts index 7127fa5350..cecf4f9d3b 100644 --- a/src/commands/manifest/run-manifest-facts.mts +++ b/src/commands/manifest/run-manifest-facts.mts @@ -236,7 +236,7 @@ export async function runManifestFacts({ } await fs.writeFile(factsPath, JSON.stringify(facts), 'utf8') - if (withFiles && sidecarAcc) { + if (sidecarAcc) { // Key by the symlink-resolved path so the sidecar's keys are comparable // regardless of which caller's cwd it was joined against (the recursive // discovery path already resolves symlinks before this point; the plain @@ -246,6 +246,7 @@ export async function runManifestFacts({ facts, artifactPaths, await realpathOrResolved(factsPath), + !!withFiles, ) } diff --git a/src/commands/manifest/scripts/sidecar.mts b/src/commands/manifest/scripts/sidecar.mts index 067f7ea3b0..53e910b1f6 100644 --- a/src/commands/manifest/scripts/sidecar.mts +++ b/src/commands/manifest/scripts/sidecar.mts @@ -99,17 +99,21 @@ function sortByPurl(entries: T[]): T[] { // roots are where reachability starts, so the sidecar must carry them. // A second call for the same factsFile (a dual-marker directory where two // build tools both target it) overwrites rather than merges, matching the -// existing last-writer-wins convention for that case. +// existing last-writer-wins convention for that case. Without `withPaths` +// (artifact paths were not resolved) entries carry only the classpaths. export function accumulateSidecar( acc: SidecarAccumulator, facts: SocketFactsSbom, artifactPaths: ResolvedArtifactPaths, factsFile: string, + withPaths = true, ): void { + const paths = (entry: T) => + withPaths ? attachPaths(entry, artifactPaths) : { ...entry } acc.set(factsFile, { - components: facts.components.map(comp => attachPaths(comp, artifactPaths)), + components: facts.components.map(paths), projects: (facts.projects ?? []).map(proj => ({ - ...attachPaths(proj, artifactPaths), + ...paths(proj), classpath: [ ...(artifactPaths.classpathByProject.get(projectClasspathKey(proj)) ?? []), diff --git a/src/commands/manifest/scripts/sidecar.test.mts b/src/commands/manifest/scripts/sidecar.test.mts index f632be6624..e818972e7c 100644 --- a/src/commands/manifest/scripts/sidecar.test.mts +++ b/src/commands/manifest/scripts/sidecar.test.mts @@ -45,6 +45,48 @@ function mkComponentFixture(target: string): { } describe('compute-artifacts sidecar', () => { + it('carries only the classpaths when artifact paths were not resolved', () => { + const facts: SocketFactsSbom = { + projects: [ + { + type: 'maven', + namespace: 'g', + name: 'app', + subprojectDir: 'app', + dependencies: ['g:a:jar:1'], + }, + ], + components: [ + { + type: 'maven', + namespace: 'g', + name: 'a', + version: '1', + qualifiers: { ext: 'jar' }, + id: 'g:a:jar:1', + }, + ], + } + const artifactPaths = emptyArtifactPaths() + artifactPaths.classpathByProject.set('app g:app', ['g:a:jar:1']) + + const acc: SidecarAccumulator = new Map() + accumulateSidecar( + acc, + facts, + artifactPaths, + '/root/.socket.facts.json', + false, + ) + const entry = serializeSidecar(acc)['/root/.socket.facts.json']! + + expect(entry.projects[0]).toEqual({ + ...facts.projects![0], + classpath: ['g:a:jar:1'], + }) + expect(entry.components[0]).toEqual(facts.components[0]) + }) + it('carries a component through with resolved targets/sources attached, keyed by its own facts file', () => { const facts: SocketFactsSbom = { components: [ diff --git a/src/commands/scan/run-dynamic-sbom-inference.mts b/src/commands/scan/run-dynamic-sbom-inference.mts index 9336c5dc58..e51fb0b41f 100644 --- a/src/commands/scan/run-dynamic-sbom-inference.mts +++ b/src/commands/scan/run-dynamic-sbom-inference.mts @@ -22,19 +22,22 @@ export async function runDynamicSbomInference({ cwd, excludePaths, sbtTmpDir, + sidecar, withFiles, }: { cwd: string excludePaths: string[] + // Collects the sidecar's per-project classpaths even without resolving + // artifact paths. + sidecar?: boolean | undefined // sbt provisions its Scala toolchain under this directory and withFiles' // artifactPaths point into it, so it must outlive whoever consumes them. // Only meaningful alongside `withFiles`. sbtTmpDir: string | undefined withFiles: boolean }): Promise { - const sidecarAcc: SidecarAccumulator | undefined = withFiles - ? new Map() - : undefined + const sidecarAcc: SidecarAccumulator | undefined = + (sidecar ?? withFiles) ? new Map() : undefined const outcomes = await generateRecursiveManifests({ cwd, excludePaths, From 6a25ceee19c6b842c6262b0a9a99dd8890353ec2 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Sat, 26 Sep 2026 16:25:33 +0200 Subject: [PATCH 4/6] chore(fix): tighten comments on dynamic SBOM inference --- src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts | 1 - src/commands/fix/generated-socket-facts.mts | 5 +---- src/commands/manifest/scripts/sidecar.mts | 2 +- 3 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts index 6a88d87196..09fe55e02e 100644 --- a/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts +++ b/src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts @@ -8,7 +8,6 @@ import { coanaFix } from './coana-fix.mts' import type { FixConfig } from './types.mts' -// Mock all external dependencies. const mockSpawnCoanaDlx = vi.hoisted(() => vi.fn()) const mockSetupSdk = vi.hoisted(() => vi.fn()) const mockFetchSupportedScanFileNames = vi.hoisted(() => vi.fn()) diff --git a/src/commands/fix/generated-socket-facts.mts b/src/commands/fix/generated-socket-facts.mts index 15d4bb2bda..93cff30351 100644 --- a/src/commands/fix/generated-socket-facts.mts +++ b/src/commands/fix/generated-socket-facts.mts @@ -11,10 +11,7 @@ export type GeneratedSocketFacts = { restore: () => Promise } -// The generated files describe the build before any fix, so they are kept -// aside for restoring after `git clean` and removed once the fix is done. -// Their sidecar gives each project's exact classpath, which the facts -// files' merged component graph over-approximates. +// Backed up so each fix attempt sees the pre-fix build after `git clean`. export async function generateSocketFactsForFix({ cwd, excludePaths, diff --git a/src/commands/manifest/scripts/sidecar.mts b/src/commands/manifest/scripts/sidecar.mts index 53e910b1f6..ed7215c2ed 100644 --- a/src/commands/manifest/scripts/sidecar.mts +++ b/src/commands/manifest/scripts/sidecar.mts @@ -100,7 +100,7 @@ function sortByPurl(entries: T[]): T[] { // A second call for the same factsFile (a dual-marker directory where two // build tools both target it) overwrites rather than merges, matching the // existing last-writer-wins convention for that case. Without `withPaths` -// (artifact paths were not resolved) entries carry only the classpaths. +// (artifact paths were not resolved) entries omit `targets` and `sources`. export function accumulateSidecar( acc: SidecarAccumulator, facts: SocketFactsSbom, From cb2f6de35a4d4b500ce34c457938169a7c60e8cc Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Sat, 26 Sep 2026 16:27:46 +0200 Subject: [PATCH 5/6] chore(manifest): document withPaths on its parameter --- src/commands/manifest/scripts/sidecar.mts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/manifest/scripts/sidecar.mts b/src/commands/manifest/scripts/sidecar.mts index ed7215c2ed..f0fd5fddf8 100644 --- a/src/commands/manifest/scripts/sidecar.mts +++ b/src/commands/manifest/scripts/sidecar.mts @@ -99,13 +99,13 @@ function sortByPurl(entries: T[]): T[] { // roots are where reachability starts, so the sidecar must carry them. // A second call for the same factsFile (a dual-marker directory where two // build tools both target it) overwrites rather than merges, matching the -// existing last-writer-wins convention for that case. Without `withPaths` -// (artifact paths were not resolved) entries omit `targets` and `sources`. +// existing last-writer-wins convention for that case. export function accumulateSidecar( acc: SidecarAccumulator, facts: SocketFactsSbom, artifactPaths: ResolvedArtifactPaths, factsFile: string, + // Off when artifact paths were not resolved; entries then omit `targets` and `sources`. withPaths = true, ): void { const paths = (entry: T) => From b9e5dfc859fa984a61716f2e282200d3804c5374 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Sat, 26 Sep 2026 18:41:44 +0200 Subject: [PATCH 6/6] bump coana --- CHANGELOG.md | 3 +++ package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71745bf08e..6f8d005336 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - `socket fix --dynamic-sbom-inference` generates Socket facts for each Gradle, sbt and Maven build and fixes a vulnerable dependency only in the projects/modules that resolve it. The generated files are removed afterwards. +### Changed +- Updated the Coana CLI to v `15.10.55`. + ### Fixed - Fixes opened as pull requests now include edits to build files that are not uploaded manifests, such as `gradle.properties` or sbt `project/*.scala` files, and the files a fix creates. diff --git a/package.json b/package.json index 0064c66ff6..6f357e3b0f 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.10.55", + "@coana-tech/cli": "15.11.0", "@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 2a321a7cff..8eaa270d7b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -138,8 +138,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.10.55 - version: 15.10.55 + specifier: 15.11.0 + version: 15.11.0 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -827,8 +827,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.10.55': - resolution: {integrity: sha512-zhWbiFA8dJUB/PelaUaMiSKfVXuF5M5KpLWuMgNamrXH5k9G+bk4BIlPPS6KvW5u/85UurzsvwAWLQOB3VeUEQ==} + '@coana-tech/cli@15.11.0': + resolution: {integrity: sha512-U0uSKbOIKo6Wc2cfw/GcHapbmlmK4NCz2+MPvAhn/ekelSF+hz0crahx2ZnyqYPz+JPW6HtiYgpm2dJSWXzjRA==} hasBin: true '@colors/colors@1.5.0': @@ -5702,7 +5702,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.10.55': {} + '@coana-tech/cli@15.11.0': {} '@colors/colors@1.5.0': optional: true