Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.5`.

## [1.1.119](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.119) - 2026-06-11

### Changed
Expand Down Expand Up @@ -101,7 +108,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
- Updated the Coana CLI to v `15.3.9`.

## [1.1.98](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.98) - 2026-05-22
## [1.1.102](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.98) - 2026-05-22

### Added
- **`socket manifest gradle --facts [beta]`** (and its `socket manifest kotlin --facts` alias) — Emit a `.socket.facts.json` dependency graph from a Gradle build for `socket scan create` to consume as a pregenerated SBOM. Toggle also exposed via the `socket manifest setup` wizard for use with `--auto-manifest`.
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "socket",
"version": "1.1.119",
"version": "1.1.120",
"description": "CLI for Socket.dev",
"homepage": "https://github.com/SocketDev/socket-cli",
"license": "MIT",
Expand Down Expand Up @@ -96,7 +96,7 @@
"@babel/preset-typescript": "7.27.1",
"@babel/runtime": "7.28.4",
"@biomejs/biome": "2.2.4",
"@coana-tech/cli": "15.3.26",
"@coana-tech/cli": "15.4.5",
"@cyclonedx/cdxgen": "12.1.2",
"@dotenvx/dotenvx": "1.49.0",
"@eslint/compat": "1.3.2",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

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

2 changes: 1 addition & 1 deletion src/commands/ci/fetch-default-org-slug.test.mts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { getDefaultOrgSlug } from './fetch-default-org-slug.mts'
import { fetchOrganization } from '../organization/fetch-organization-list.mts'
import { getConfigValueOrUndef } from '../../utils/config.mts'
import { fetchOrganization } from '../organization/fetch-organization-list.mts'

vi.mock('../organization/fetch-organization-list.mts', () => ({
fetchOrganization: vi.fn(),
Expand Down
10 changes: 10 additions & 0 deletions src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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),
Expand Down
116 changes: 78 additions & 38 deletions src/commands/scan/perform-reachability-analysis.mts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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 in the finally 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',
Expand Down Expand Up @@ -228,6 +252,11 @@ 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.
...(autoManifestConfigPath
? ['--auto-manifest-config', autoManifestConfigPath]
: []),
Comment thread
jfblaa marked this conversation as resolved.
]

// Build environment variables.
Expand All @@ -241,48 +270,59 @@ export async function performReachabilityAnalysis(
coanaEnv['SOCKET_BRANCH_NAME'] = branchName
}

// Run Coana with the manifests tar hash.
const coanaResult = await spawnCoanaDlx(coanaArgs, orgSlug, {
coanaVersion: reachabilityOptions.reachVersion,
cwd,
env: coanaEnv,
spinner,
stdio: 'inherit',
})
try {
// Run Coana with the manifests tar hash.
const coanaResult = await spawnCoanaDlx(coanaArgs, orgSlug, {
coanaVersion: reachabilityOptions.reachVersion,
cwd,
env: coanaEnv,
spinner,
stdio: 'inherit',
})

if (wasSpinning) {
spinner.start()
}
if (wasSpinning) {
spinner.start()
}

if (!coanaResult.ok) {
const coanaVersion =
reachabilityOptions.reachVersion ||
constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION
logger.error(
`Coana reachability analysis failed. Version: ${coanaVersion}, target: ${analysisTarget}, cwd: ${cwd}`,
)
if (coanaResult.message) {
logger.error(`Details: ${coanaResult.message}`)
if (!coanaResult.ok) {
const coanaVersion =
reachabilityOptions.reachVersion ||
constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION
logger.error(
`Coana reachability analysis failed. Version: ${coanaVersion}, target: ${analysisTarget}, cwd: ${cwd}`,
)
if (coanaResult.message) {
logger.error(`Details: ${coanaResult.message}`)
}
return coanaResult
}
return coanaResult
}

// Coana writes the facts file relative to the scan `cwd` (it is spawned
// with `cwd` above), so resolve the read path against `cwd` too. Reading
// the bare relative path would resolve against `process.cwd()` and miss
// the file whenever `cwd !== process.cwd()` (e.g. `--cwd <dir>`), silently
// dropping the tier 1 scan id and skipping finalize downstream.
const resolvedReportPath = path.resolve(cwd, outputFilePath)
// Coana writes the facts file relative to the scan `cwd` (it is spawned
// with `cwd` above), so resolve the read path against `cwd` too. Reading
// the bare relative path would resolve against `process.cwd()` and miss
// the file whenever `cwd !== process.cwd()` (e.g. `--cwd <dir>`), silently
// dropping the tier 1 scan id and skipping finalize downstream.
const resolvedReportPath = path.resolve(cwd, outputFilePath)

return {
ok: true,
data: {
// Use the actual output filename for the scan. Keep this `cwd`-relative
// so the upload (which relativizes against `cwd`) and the post-success
// unlink (`path.resolve(cwd, reachabilityReport)`) keep working.
reachabilityReport: outputFilePath,
tier1ReachabilityScanId:
extractTier1ReachabilityScanId(resolvedReportPath),
},
return {
ok: true,
data: {
// Use the actual output filename for the scan. Keep this `cwd`-relative
// so the upload (which relativizes against `cwd`) and the post-success
// unlink (`path.resolve(cwd, reachabilityReport)`) keep working.
reachabilityReport: outputFilePath,
tier1ReachabilityScanId:
extractTier1ReachabilityScanId(resolvedReportPath),
},
}
} finally {
// 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.
}
}
}
}
110 changes: 110 additions & 0 deletions src/utils/auto-manifest-config.mts
Original file line number Diff line number Diff line change
@@ -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 <ecosystem>`) and, in socket mode, for
// reach-time dependency resolution (`coana run`). This mirrors the Coana-side
// `--auto-manifest-config` shape: 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.<ecosystem>` 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
)
}
Loading
Loading