Skip to content
Closed
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ 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]

### Fixed
- One unresolvable Maven dependency no longer strips the jar paths from every other dependency of its build, which made reachability analysis call their vulnerabilities unreachable.
- Maven, Gradle and sbt dependencies whose jars cannot be fetched are reported, and reachability no longer treats them as resolved without code. sbt no longer passes them off as POM-only modules.
- Reachability scans that hit unresolved JVM dependencies now point out that ignoring them also needs `--reach-continue-on-install-errors`.
- Gradle dependencies that fail dependency verification are reported instead of passed on as healthy.
- Maven dependencies with missing or invalid POMs are reported in every module that uses them, `system`-scope jars reach reachability analysis, and Maven 4 failures show their real cause.
- Failed Gradle project dependencies are reported instead of silently dropping everything they pull in, and failures name the version that actually failed.
- A module excluded with `--exclude-paths` is still analysed as part of the projects that depend on it.
- A failing dependency no longer fails scopes or configurations that `excludeConfigs` leaves out, and a version-conflict loser no longer fails the build.
- With `--dynamic-sbom-inference`, an sbt build that fails before writing its results is reported as failed instead of reusing another build's results.

## [1.10.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.10.0) - 2026-10-22

### Added
Expand Down
5 changes: 3 additions & 2 deletions src/commands/manifest/convert-gradle-to-facts.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { runManifestFacts } from './run-manifest-facts.mts'

import type { RunManifestFactsOutcome } from './run-manifest-facts.mts'
import type { BuildOptionHints } from './scripts/resolution-report-render.mts'
import type { SidecarAccumulator } from './scripts/sidecar.mts'

Expand Down Expand Up @@ -31,8 +32,8 @@ export async function convertGradleToFacts({
sidecarAcc?: SidecarAccumulator | undefined
verbose: boolean
withFiles?: boolean | undefined
}): Promise<void> {
await runManifestFacts({
}): Promise<RunManifestFactsOutcome> {
return await runManifestFacts({
bin,
buildOpts: gradleOpts,
cwd,
Expand Down
5 changes: 3 additions & 2 deletions src/commands/manifest/convert-maven-to-facts.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { runManifestFacts } from './run-manifest-facts.mts'

import type { RunManifestFactsOutcome } from './run-manifest-facts.mts'
import type { BuildOptionHints } from './scripts/resolution-report-render.mts'
import type { SidecarAccumulator } from './scripts/sidecar.mts'

Expand Down Expand Up @@ -31,8 +32,8 @@ export async function convertMavenToFacts({
sidecarAcc?: SidecarAccumulator | undefined
verbose: boolean
withFiles?: boolean | undefined
}): Promise<void> {
await runManifestFacts({
}): Promise<RunManifestFactsOutcome> {
return await runManifestFacts({
bin,
buildOpts: mavenOpts,
cwd,
Expand Down
5 changes: 3 additions & 2 deletions src/commands/manifest/convert-sbt-to-facts.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { runManifestFacts } from './run-manifest-facts.mts'

import type { RunManifestFactsOutcome } from './run-manifest-facts.mts'
import type { BuildOptionHints } from './scripts/resolution-report-render.mts'
import type { SidecarAccumulator } from './scripts/sidecar.mts'

Expand Down Expand Up @@ -35,8 +36,8 @@ export async function convertSbtToFacts({
tmpDir: string
verbose: boolean
withFiles?: boolean | undefined
}): Promise<void> {
await runManifestFacts({
}): Promise<RunManifestFactsOutcome> {
return await runManifestFacts({
bin,
buildOpts: sbtOpts,
cwd,
Expand Down
16 changes: 11 additions & 5 deletions src/commands/manifest/generate-recursive-manifests.mts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ import {
import { parseBuildToolOpts } from './parse-build-tool-opts.mts'
import { runManifestFacts } from './run-manifest-facts.mts'
import { resolveBuildToolBin } from './scripts/build-tool.mts'
import { DSI_SETUP_COMMAND } from './scripts/resolution-report-render.mts'
import {
DSI_SETUP_COMMAND,
REACH_CONTINUE_ON_INSTALL_ERRORS_FLAG,
} from './scripts/resolution-report-render.mts'
import { SOCKET_JSON } from '../../constants.mts'
import { withTmpDir } from '../../utils/fs.mts'
import {
Expand Down Expand Up @@ -179,6 +182,9 @@ async function runEcosystemCandidates({
excludeConfigs,
excludePaths: excludePathsForRoot,
hints: {
continueOnInstallErrorsFlag: withFiles
? REACH_CONTINUE_ON_INSTALL_ERRORS_FLAG
: undefined,
flags: optionFlags,
setupCommand: DSI_SETUP_COMMAND,
socketJson: displayPath(path.join(dir, SOCKET_JSON)),
Expand Down Expand Up @@ -294,10 +300,10 @@ export async function generateRecursiveManifests({
// A shared global base across every sbt root in this run lets sbt's own
// Scala-toolchain cache under <base>/boot survive between invocations
// instead of being reprovisioned per root (the plugin file is rewritten and
// records.tsv is fully overwritten - not appended - on every invocation, so
// reuse is safe). A caller-supplied sbtTmpDir is reused as-is (the caller
// owns its lifetime); otherwise one is allocated and cleaned up here, but
// only when there's an sbt root to benefit from sharing it at all.
// records.tsv is removed before every invocation, so reuse is safe). A
// caller-supplied sbtTmpDir is reused as-is (the caller owns its lifetime);
// otherwise one is allocated and cleaned up here, but only when there's an
// sbt root to benefit from sharing it at all.
const outcomes = sbtTmpDir
? await runAll(sbtTmpDir)
: candidatesByTool.get('sbt')?.length
Expand Down
49 changes: 31 additions & 18 deletions src/commands/manifest/generate_auto_manifest.mts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { convertSbtToMaven } from './convert_sbt_to_maven.mts'
import { handleManifestConda } from './handle-manifest-conda.mts'
import { parseBuildToolOpts } from './parse-build-tool-opts.mts'
import { resolveBuildToolBin } from './scripts/build-tool.mts'
import { REACH_CONTINUE_ON_INSTALL_ERRORS_FLAG } from './scripts/resolution-report-render.mts'
import { hasSidecarEntries, serializeSidecar } from './scripts/sidecar.mts'
import {
ENVIRONMENT_YML,
Expand All @@ -21,6 +22,7 @@ import { InputError } from '../../utils/errors.mts'
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'

import type { GeneratableManifests } from './detect-manifest-actions.mts'
import type { RunManifestFactsOutcome } from './run-manifest-facts.mts'
import type { BuildOptionHints } from './scripts/resolution-report-render.mts'
import type {
ResolvedPathsSidecar,
Expand All @@ -41,12 +43,20 @@ export type GenerateAutoManifestResult = {
resolvedPathsSidecar?: ResolvedPathsSidecar | undefined
}

// Under --auto-manifest, a manifest generator that failed — raising the exit
// code above the value captured before it ran — aborts the whole run: a partial
// or empty SBOM silently under-reports dependencies. The generator has already
// logged the specifics. A tolerated resolution failure (ignoreUnresolved) warns
// without touching the exit code, so it passes through here and the run
// continues.
// A failed generator aborts --auto-manifest: a partial SBOM silently
// under-reports dependencies. Facts generators return null on failure; the
// others only change the exit code.
function abortFactsRunIfFailed(
ecosystem: string,
outcome: RunManifestFactsOutcome,
): void {
if (outcome === null) {
throw new InputError(
`Auto-manifest generation failed for the ${ecosystem} project; aborting (see the errors above).`,
)
}
}

function abortManifestRunIfFailed(
ecosystem: string,
beforeExitCode: string | number | undefined,
Expand Down Expand Up @@ -82,6 +92,12 @@ export async function generateAutoManifest({
}): Promise<GenerateAutoManifestResult> {
const sockJson = readOrDefaultSocketJson(cwd)
const generatedFiles: string[] = []
const hints: BuildOptionHints = computeArtifactsSidecar
? {
...AUTO_MANIFEST_HINTS,
continueOnInstallErrorsFlag: REACH_CONTINUE_ON_INSTALL_ERRORS_FLAG,
}
: AUTO_MANIFEST_HINTS

// Resolved paths across all JVM roots, serialized to one sidecar at the end.
const sidecarAcc: SidecarAccumulator | undefined = computeArtifactsSidecar
Expand Down Expand Up @@ -109,8 +125,7 @@ export async function generateAutoManifest({
// `defaults.manifest.sbt.facts: false` in socket.json.
if (sockJson.defaults?.manifest?.sbt?.facts !== false) {
logger.log('Detected a Scala sbt build, generating Socket facts...')
const beforeExitCode = process.exitCode
await convertSbtToFacts({
const outcome = await convertSbtToFacts({
...sbtArgs,
excludeConfigs: sockJson.defaults?.manifest?.sbt?.excludeConfigs ?? '',
excludePaths,
Expand All @@ -119,12 +134,12 @@ export async function generateAutoManifest({
),
includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '',
javaHome: sockJson.defaults?.manifest?.sbt?.javaHome ?? undefined,
hints: AUTO_MANIFEST_HINTS,
hints,
sidecarAcc,
tmpDir,
withFiles: computeArtifactsSidecar,
})
abortManifestRunIfFailed('sbt', beforeExitCode)
abortFactsRunIfFailed('sbt', outcome)
} else {
logger.log('Detected a Scala sbt build, generating pom files with sbt...')
const beforeExitCode = process.exitCode
Expand Down Expand Up @@ -154,8 +169,7 @@ export async function generateAutoManifest({
logger.log(
'Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...',
)
const beforeExitCode = process.exitCode
await convertGradleToFacts({
const outcome = await convertGradleToFacts({
...gradleArgs,
excludeConfigs:
sockJson.defaults?.manifest?.gradle?.excludeConfigs ?? '',
Expand All @@ -166,11 +180,11 @@ export async function generateAutoManifest({
includeConfigs:
sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '',
javaHome: sockJson.defaults?.manifest?.gradle?.javaHome ?? undefined,
hints: AUTO_MANIFEST_HINTS,
hints,
sidecarAcc,
withFiles: computeArtifactsSidecar,
})
abortManifestRunIfFailed('gradle', beforeExitCode)
abortFactsRunIfFailed('gradle', outcome)
} else {
logger.log(
'Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...',
Expand All @@ -183,8 +197,7 @@ export async function generateAutoManifest({

if (!sockJson?.defaults?.manifest?.maven?.disabled && detected.maven) {
logger.log('Detected a Maven pom.xml build, generating Socket facts...')
const beforeExitCode = process.exitCode
await convertMavenToFacts({
const outcome = await convertMavenToFacts({
// Configured bin wins; else prefer ./mvnw, else mvn on PATH.
bin:
sockJson.defaults?.manifest?.maven?.bin ??
Expand All @@ -200,12 +213,12 @@ export async function generateAutoManifest({
mavenOpts: parseBuildToolOpts(
sockJson.defaults?.manifest?.maven?.mavenOpts ?? undefined,
),
hints: AUTO_MANIFEST_HINTS,
hints,
sidecarAcc,
verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose),
withFiles: computeArtifactsSidecar,
})
abortManifestRunIfFailed('maven', beforeExitCode)
abortFactsRunIfFailed('maven', outcome)
}

if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) {
Expand Down
14 changes: 14 additions & 0 deletions src/commands/manifest/generate_auto_manifest.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,18 @@ describe('generateAutoManifest — conda branch', () => {
).rejects.toThrow(/Auto-manifest generation failed for the conda project/)
process.exitCode = undefined
})

it('aborts on a failed facts generator even when the exit code was already set', async () => {
process.exitCode = 1
vi.mocked(convertGradleToFacts).mockResolvedValueOnce(null)
await expect(
generateAutoManifest({
cwd: '/tmp/repo',
detected: { ...baseDetected, gradle: true, count: 1 },
outputKind: 'text',
verbose: false,
}),
).rejects.toThrow(/Auto-manifest generation failed for the gradle project/)
process.exitCode = undefined
})
})
7 changes: 6 additions & 1 deletion src/commands/manifest/run-manifest-facts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,12 @@ export async function runManifestFacts({
report.failures,
report.scannedConfigs,
ecosystem,
{ hints, ignoreUnresolved, unscannable: report.unscannable },
{
hints,
ignoreUnresolved,
resolvedModules: report.resolvedModules,
unscannable: report.unscannable,
},
)

if (rendered.hasBlockingFailures) {
Expand Down
7 changes: 6 additions & 1 deletion src/commands/manifest/run-manifest-facts.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ function okResult(buildRoot: string): ManifestRunResult {
projects: [],
},
factsFileName: 'pom.xml.socket.facts.json',
report: { failures: [], scannedConfigs: [], unscannable: [] },
report: {
failures: [],
resolvedModules: new Set(),
scannedConfigs: [],
unscannable: [],
},
artifactPaths: {
pathsById: new Map(),
classpathByProject: new Map(),
Expand Down
Loading