From fe8f0760cda62b3721ce1aa4e68adcf8fdd294ef Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Thu, 11 Jun 2026 15:03:11 +0200 Subject: [PATCH 1/2] feat(scan): forward socket.json build-tool config into reachability socket scan create --reach now maps socket.json's per-ecosystem manifest build-tool options (bin, include/exclude-configs, gradle/sbt opts) into a Coana-defined AutoManifestConfig and passes it to `coana run` via --auto-manifest-config (a temp JSON file path Coana reads), so reach-time dependency resolution invokes the build tool the way the project is configured rather than with defaults. Under --auto-manifest the config also carries top-level failOnBuildToolError=true (fail-closed: Coana treats a build-tool step failure as fatal instead of tolerating it); plain --reach leaves it unset and stays permissive. This is the socket-cli side of the manifest-flag-propagation gap. The Coana `--auto-manifest-config` option is not yet released, so this must not ship until Coana publishes it and the pinned @coana-tech/cli is bumped; until then it is exercised via SOCKET_CLI_COANA_LOCAL_PATH. - add src/utils/auto-manifest-config.mts: BuildToolOptions/AutoManifestConfig types + buildAutoManifestConfig (socket.json -> config) + tests - ReachabilityOptions.autoManifestConfig; write the config to a temp file and pass its path to coana run, cleaning it up after - build the config at the cmd-scan-create assembly point --- src/commands/scan/cmd-scan-create.mts | 10 ++ .../scan/perform-reachability-analysis.mts | 39 +++++++ src/utils/auto-manifest-config.mts | 110 ++++++++++++++++++ src/utils/auto-manifest-config.test.mts | 95 +++++++++++++++ 4 files changed, 254 insertions(+) create mode 100644 src/utils/auto-manifest-config.mts create mode 100644 src/utils/auto-manifest-config.test.mts diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index db404f4a9..4221a52b6 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -13,6 +13,7 @@ import { suggestTarget } from './suggest_target.mts' import { validateReachabilityTarget } from './validate-reachability-target.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags, outputFlags } from '../../flags.mts' +import { buildAutoManifestConfig } from '../../utils/auto-manifest-config.mts' import { checkCommandInput } from '../../utils/check-input.mts' import { cmdFlagValueToArray } from '../../utils/cmd.mts' import { determineOrgSlug } from '../../utils/determine-org-slug.mts' @@ -622,6 +623,15 @@ async function run( pendingHead: Boolean(pendingHead), pullRequest: Number(pullRequest), reach: { + // Build-tool config for the reach-time resolution, mapped from socket.json + // (per-ecosystem). Best-effort on plain --reach; under --auto-manifest the + // config carries top-level failOnBuildToolError=true (fail-closed). Only + // built when reachability runs. + autoManifestConfig: reach + ? buildAutoManifestConfig(sockJson, { + autoManifest: Boolean(autoManifest), + }) + : undefined, excludePaths, reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit), reachAnalysisTimeout: Number(reachAnalysisTimeout), diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 0c623dab9..267a4186b 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -1,9 +1,13 @@ +import { randomUUID } from 'node:crypto' +import { promises as fs } from 'node:fs' +import { tmpdir } from 'node:os' import path from 'node:path' import { logger } from '@socketsecurity/registry/lib/logger' import constants from '../../constants.mts' import { handleApiCall } from '../../utils/api.mts' +import { isAutoManifestConfigEmpty } from '../../utils/auto-manifest-config.mts' import { extractTier1ReachabilityScanId } from '../../utils/coana.mts' import { spawnCoanaDlx } from '../../utils/dlx.mts' import { hasEnterpriseOrgPlan } from '../../utils/organization.mts' @@ -12,10 +16,12 @@ import { socketDevLink } from '../../utils/terminal-link.mts' import { fetchOrganization } from '../organization/fetch-organization-list.mts' import type { CResult } from '../../types.mts' +import type { AutoManifestConfig } from '../../utils/auto-manifest-config.mts' import type { PURL_Type } from '../../utils/ecosystem.mts' import type { Spinner } from '@socketsecurity/registry/lib/spinner' export type ReachabilityOptions = { + autoManifestConfig?: AutoManifestConfig | undefined excludePaths: string[] reachAnalysisMemoryLimit: number reachAnalysisTimeout: number @@ -170,6 +176,24 @@ export async function performReachabilityAnalysis( spinner?.infoAndStop('Running reachability analysis with Coana...') const outputFilePath = outputPath || constants.DOT_SOCKET_DOT_FACTS_JSON + + // Coana reads `--auto-manifest-config` from a JSON file, so write the resolved + // per-ecosystem build-tool config (mapped from socket.json) to a temp file and + // pass its absolute path. Cleaned up right after the run below. + let autoManifestConfigPath: string | undefined + const { autoManifestConfig } = reachabilityOptions + if (autoManifestConfig && !isAutoManifestConfigEmpty(autoManifestConfig)) { + autoManifestConfigPath = path.join( + tmpdir(), + `socket-auto-manifest-config-${randomUUID()}.json`, + ) + await fs.writeFile( + autoManifestConfigPath, + JSON.stringify(autoManifestConfig), + 'utf8', + ) + } + // Build Coana arguments. const coanaArgs = [ 'run', @@ -228,6 +252,12 @@ export async function performReachabilityAnalysis( ...(reachabilityOptions.reachUseOnlyPregeneratedSboms ? ['--use-only-pregenerated-sboms'] : []), + // Hand the per-ecosystem build-tool config (mapped from socket.json) to + // Coana's reach-time resolution, as a temp JSON file path. Coana side: + // REA-547. + ...(autoManifestConfigPath + ? ['--auto-manifest-config', autoManifestConfigPath] + : []), ] // Build environment variables. @@ -250,6 +280,15 @@ export async function performReachabilityAnalysis( stdio: 'inherit', }) + // The run no longer needs the temp config file; best-effort cleanup. + if (autoManifestConfigPath) { + try { + await fs.unlink(autoManifestConfigPath) + } catch { + // File may already be gone or unwritable. + } + } + if (wasSpinning) { spinner.start() } diff --git a/src/utils/auto-manifest-config.mts b/src/utils/auto-manifest-config.mts new file mode 100644 index 000000000..6a911107e --- /dev/null +++ b/src/utils/auto-manifest-config.mts @@ -0,0 +1,110 @@ +import type { SocketJson } from './socket-json.mts' + +// Per-ecosystem build-tool options handed off to the Coana CLI — used both when +// generating manifests (`coana manifest `) and, in socket mode, for +// reach-time dependency resolution (`coana run`). This mirrors the Coana-side +// `--auto-manifest-config` shape (REA-547): socket-cli owns mapping `socket.json` +// onto it, so Coana stays uncoupled from `socket.json`'s schema. Keeping the +// per-ecosystem options namespaced (rather than as flat CLI flags) avoids the +// ambiguity of a bare `--bin`/`--include-configs` when a repo has more than one +// build tool. +export type BuildToolOptions = { + // Build-tool executable override (e.g. `./gradlew`, `atlas-mvn`). + bin?: string | undefined + // Comma-separated config-name globs to skip. + excludeConfigs?: string | undefined + // `socket.json`'s per-ecosystem `ignoreUnresolved` (warn vs fail on unresolved + // dependencies), forwarded verbatim. NOTE: this is NOT the reach-time + // fail-closed switch — that's the run-wide `failOnBuildToolError` below. + ignoreUnresolved?: boolean | undefined + // Comma-separated config-name globs to resolve. + includeConfigs?: string | undefined + // Extra build-tool options, pre-split into argv. Coana maps these straight to + // the tool's opts (no splitting on its side). Mapped from `socket.json`'s + // `gradleOpts`/`sbtOpts` string. + opts?: string[] | undefined +} + +// The Coana hand-off config. `failOnBuildToolError` is run-wide (top level) +// because `--auto-manifest` is a single CLI mode, not a per-package-manager +// setting. The per-ecosystem entries are present only for ecosystems configured +// (and not disabled) in `socket.json`; absent ecosystems fall to Coana's own +// defaults. +export type AutoManifestConfig = { + // Run-wide fail-closed switch. When true, Coana treats a build-tool step + // failure as fatal rather than tolerating it. socket-cli sets it true under + // `--auto-manifest`; left unset on plain `--reach` (permissive — Coana's + // default best-effort behaviour). + failOnBuildToolError?: boolean | undefined + gradle?: BuildToolOptions | undefined + sbt?: BuildToolOptions | undefined +} + +// Splits a `socket.json` opts string (`gradleOpts`/`sbtOpts`) into argv, matching +// how the standalone `socket manifest` path splits it. Returns undefined when +// there's nothing to pass so the field is omitted from the config. +function parseOpts(value: string | undefined): string[] | undefined { + if (!value) { + return undefined + } + const parts = value + .split(' ') + .map(s => s.trim()) + .filter(Boolean) + return parts.length ? parts : undefined +} + +// Maps `socket.json`'s `defaults.manifest.` build-tool options onto +// the Coana hand-off config. +// +// `autoManifest` reflects whether the run is `--auto-manifest` (fail-closed: +// `failOnBuildToolError=true`) vs plain `--reach` (permissive: +// `failOnBuildToolError` left unset so Coana's default applies). Per-ecosystem +// options are forwarded verbatim from `socket.json`; disabled ecosystems are +// omitted so they fall back to Coana's defaults. +export function buildAutoManifestConfig( + sockJson: SocketJson, + { autoManifest }: { autoManifest: boolean }, +): AutoManifestConfig { + const manifest = sockJson.defaults?.manifest + const config: AutoManifestConfig = {} + + // `--auto-manifest` expects every build-tool command to succeed, so a + // build-tool step failure should be fatal rather than tolerated. + if (autoManifest) { + config.failOnBuildToolError = true + } + + const gradle = manifest?.gradle + if (gradle && !gradle.disabled) { + config.gradle = { + bin: gradle.bin, + excludeConfigs: gradle.excludeConfigs, + ignoreUnresolved: gradle.ignoreUnresolved, + includeConfigs: gradle.includeConfigs, + opts: parseOpts(gradle.gradleOpts), + } + } + + const sbt = manifest?.sbt + if (sbt && !sbt.disabled) { + config.sbt = { + bin: sbt.bin, + excludeConfigs: sbt.excludeConfigs, + ignoreUnresolved: sbt.ignoreUnresolved, + includeConfigs: sbt.includeConfigs, + opts: parseOpts(sbt.sbtOpts), + } + } + + return config +} + +// True when there's nothing to hand to Coana: no per-ecosystem options and the +// run mode is left at Coana's permissive default. When true, the +// `--auto-manifest-config` option should be omitted entirely. +export function isAutoManifestConfigEmpty(config: AutoManifestConfig): boolean { + return ( + !config.gradle && !config.sbt && config.failOnBuildToolError === undefined + ) +} diff --git a/src/utils/auto-manifest-config.test.mts b/src/utils/auto-manifest-config.test.mts new file mode 100644 index 000000000..f0ffdc600 --- /dev/null +++ b/src/utils/auto-manifest-config.test.mts @@ -0,0 +1,95 @@ +import { describe, expect, it } from 'vitest' + +import { + buildAutoManifestConfig, + isAutoManifestConfigEmpty, +} from './auto-manifest-config.mts' + +import type { SocketJson } from './socket-json.mts' + +// Builds a minimal SocketJson for the mapping under test; only +// `defaults.manifest` is read, so the header/version fields are irrelevant. +function socketJson( + manifest?: NonNullable['manifest']>, +): SocketJson { + return { defaults: { manifest } } as SocketJson +} + +describe('buildAutoManifestConfig', () => { + it('returns an empty config for plain --reach with no manifest defaults', () => { + expect( + buildAutoManifestConfig(socketJson(), { autoManifest: false }), + ).toEqual({}) + }) + + it('sets top-level failOnBuildToolError=true under --auto-manifest (fail-closed)', () => { + expect( + buildAutoManifestConfig(socketJson(), { autoManifest: true }), + ).toEqual({ failOnBuildToolError: true }) + }) + + it('leaves failOnBuildToolError unset on plain --reach (Coana default permissive)', () => { + const config = buildAutoManifestConfig( + socketJson({ gradle: { bin: './gradlew' } }), + { autoManifest: false }, + ) + expect(config.failOnBuildToolError).toBeUndefined() + }) + + it('maps gradle/sbt options, *Opts -> opts, ignoreUnresolved passthrough', () => { + const config = buildAutoManifestConfig( + socketJson({ + gradle: { + bin: './gradlew', + excludeConfigs: 'testCompileClasspath', + gradleOpts: '--offline --no-daemon', + ignoreUnresolved: true, + includeConfigs: '*RuntimeClasspath', + }, + sbt: { bin: 'sbt', sbtOpts: '-batch' }, + }), + { autoManifest: true }, + ) + expect(config).toEqual({ + failOnBuildToolError: true, + gradle: { + bin: './gradlew', + excludeConfigs: 'testCompileClasspath', + ignoreUnresolved: true, + includeConfigs: '*RuntimeClasspath', + opts: ['--offline', '--no-daemon'], + }, + sbt: { bin: 'sbt', opts: ['-batch'] }, + }) + }) + + it('omits disabled ecosystems so they fall back to Coana defaults', () => { + const config = buildAutoManifestConfig( + socketJson({ + gradle: { disabled: true, includeConfigs: '*RuntimeClasspath' }, + sbt: { bin: 'sbt' }, + }), + { autoManifest: false }, + ) + expect(config.gradle).toBeUndefined() + expect(config.sbt).toBeDefined() + }) +}) + +describe('isAutoManifestConfigEmpty', () => { + it('is true when there are no ecosystems and the run mode is default', () => { + expect(isAutoManifestConfigEmpty({})).toBe(true) + }) + + it('is false when failOnBuildToolError is set (fail-closed must reach Coana)', () => { + expect(isAutoManifestConfigEmpty({ failOnBuildToolError: true })).toBe( + false, + ) + }) + + it('is false when an ecosystem is configured', () => { + expect(isAutoManifestConfigEmpty({ gradle: { bin: './gradlew' } })).toBe( + false, + ) + }) +}) From 478fb22587ad32c8d1270220c656a9a91a06f42e Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Fri, 12 Jun 2026 14:40:43 +0200 Subject: [PATCH 2/2] =?UTF-8?q?chore(release):=201.1.120=20=E2=80=94=20Coa?= =?UTF-8?q?na=2015.4.1=20and=20socket.json=20build-tool=20config=20forward?= =?UTF-8?q?ing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump @coana-tech/cli to 15.4.1 (which ships the --auto-manifest-config option the feat commit depends on), bump the package version to 1.1.120, and add the changelog entry. --- CHANGELOG.md | 7 +++++++ package.json | 4 ++-- pnpm-lock.yaml | 10 +++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bc540168..63e031ade 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,13 @@ 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.120](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.120) - 2026-06-12 + +### Changed +- `socket scan create --reach` now applies your project's build-tool settings from `socket.json` (configured via `socket manifest setup`) — custom build-tool binary, include/exclude configs, and Gradle/sbt options — when resolving dependencies for Gradle and sbt reachability analysis, instead of always invoking the build tool with defaults. +- `socket scan create --auto-manifest --reach` now fails with an error when a build tool fails during manifest generation, rather than tolerating it. Plain `--reach` (without `--auto-manifest`) keeps generating manifests on a best-effort basis. +- Updated the Coana CLI to v `15.4.1`. + ## [1.1.119](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.119) - 2026-06-11 ### Changed diff --git a/package.json b/package.json index 82975296c..b42947fbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.119", + "version": "1.1.120", "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.3.26", + "@coana-tech/cli": "15.4.1", "@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 3d3197590..5960aad92 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.3.26 - version: 15.3.26 + specifier: 15.4.1 + version: 15.4.1 '@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.3.26': - resolution: {integrity: sha512-l7Jnto1dCOUKVSxdzfJhmv+6VHi57b00z5IjTda45XxbSGVirFJUYxdhNMXjijYYQHc1bUf0b1Nh9VNWhZryug==} + '@coana-tech/cli@15.4.1': + resolution: {integrity: sha512-JvOz3ST9yN+DBWve92JaFt7zeWLEq0X94G5lSo4sNePlZLyfLwOEMnsGTcPBYsJ7SNIwBxwgqkm8/JfczWviqw==} 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.3.26': {} + '@coana-tech/cli@15.4.1': {} '@colors/colors@1.5.0': optional: true