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: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ 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.150](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.150) - 2026-07-29

### Changed
- Updated the Coana CLI to v `15.9.7`.
- `socket fix` vulnerability discovery now reads Coana's structured `--output-file` JSON result instead of parsing stdout, and warns when the Socket backend resolved 0 artifacts so an incomplete server-side resolve is surfaced instead of silently reporting "Finished!".

### Fixed
- `socket fix` no longer reports success when the Coana vulnerability-discovery step fails — Coana errors and unreadable discovery output now exit non-zero with the underlying reason instead of printing "Finished!" with nothing fixed.

## [1.1.149](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.149) - 2026-07-29

### Changed
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.149",
"version": "1.1.150",
"description": "CLI for Socket.dev",
"homepage": "https://github.com/SocketDev/socket-cli",
"license": "MIT",
Expand Down Expand Up @@ -97,7 +97,7 @@
"@babel/preset-typescript": "7.27.1",
"@babel/runtime": "7.28.4",
"@biomejs/biome": "2.2.4",
"@coana-tech/cli": "15.9.6",
"@coana-tech/cli": "15.9.7",
"@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.

48 changes: 24 additions & 24 deletions src/commands/fix/cmd-fix.e2e.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -196,9 +196,9 @@ describe('socket fix (E2E tests)', async () => {
`\nSuccessfully upgraded lodash from ${beforeVersion} to ${afterVersion}`,
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down Expand Up @@ -276,9 +276,9 @@ describe('socket fix (E2E tests)', async () => {
`\nSuccessfully upgraded lodash from ${beforeVersion} to ${afterVersion} and wrote output file`,
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down Expand Up @@ -334,9 +334,9 @@ describe('socket fix (E2E tests)', async () => {
`\nSuccessfully fixed GHSA-35jh-r3h4-6jhm by upgrading lodash from ${beforeVersion} to ${afterVersion}`,
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down Expand Up @@ -392,9 +392,9 @@ describe('socket fix (E2E tests)', async () => {
`\nSuccessfully converted CVE-2021-23337 to GHSA and fixed by upgrading lodash from ${beforeVersion} to ${afterVersion}`,
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down Expand Up @@ -458,9 +458,9 @@ describe('socket fix (E2E tests)', async () => {
'\nSuccessfully verified --silence --json outputs only JSON',
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down Expand Up @@ -525,9 +525,9 @@ describe('socket fix (E2E tests)', async () => {
`\n--package-managers NPM upgraded npm-app lodash to ${afterLodash} and left pnpm-app axios untouched`,
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down Expand Up @@ -590,9 +590,9 @@ describe('socket fix (E2E tests)', async () => {
`\n--package-managers PNPM upgraded pnpm-app axios to ${afterAxios} and left npm-app lodash untouched`,
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down Expand Up @@ -692,9 +692,9 @@ describe('socket fix (E2E tests)', async () => {
`\nSuccessfully upgraded django from ${beforeVersion} to ${afterVersion}`,
)
} catch (e) {
if (code !== 0) {
logCommandOutput(code, stdout, stderr)
}
// Log output on any failure — including exit-0 runs whose later
// assertions fail (e.g. a silent no-op upgrade).
logCommandOutput(code, stdout, stderr)
throw e
} finally {
await tempFixture.cleanup()
Expand Down
152 changes: 120 additions & 32 deletions src/commands/fix/coana-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,67 @@ type DiscoverGhsaIdsOptions = {
spinner?: Spinner | undefined
}

type StructuredDiscoveryResult = {
ghsaIds?: unknown
artifactCount?: unknown
filteredArtifactCount?: unknown
}

async function readStructuredDiscoveryResult(
outputFile: string,
silence: boolean,
): Promise<CResult<string[]>> {
let raw: string
try {
raw = await fs.readFile(outputFile, 'utf8')
} catch {
return {
ok: false,
message: 'Coana did not write a vulnerability discovery result file',
cause: `Expected \`find-vulnerabilities --output-file\` to create ${outputFile}.`,
}
}

let parsed: unknown
try {
parsed = JSON.parse(raw)
} catch {
return {
ok: false,
message:
'Could not parse the vulnerability discovery result written by Coana',
cause: `Expected JSON in ${outputFile}, got:\n ${raw.slice(0, 500)}`,
}
}

if (parsed === null || typeof parsed !== 'object') {
return {
ok: false,
message: 'Coana wrote an unexpected vulnerability discovery result',
cause: `Expected a JSON object with a ghsaIds string array, got:\n ${raw.slice(0, 500)}`,
}
}

const { artifactCount, ghsaIds } = parsed as StructuredDiscoveryResult
if (!Array.isArray(ghsaIds) || ghsaIds.some(id => typeof id !== 'string')) {
return {
ok: false,
message: 'Coana wrote an unexpected vulnerability discovery result',
cause: `Expected a JSON object with a ghsaIds string array, got:\n ${raw.slice(0, 500)}`,
}
}
Comment thread
mtorp marked this conversation as resolved.

// Zero artifacts means the backend resolved nothing — suspicious when
// manifests were uploaded, so warn even though discovery itself succeeded.
if (artifactCount === 0 && !silence) {
logger.warn(
'The Socket backend resolved 0 artifacts, so vulnerability discovery may be incomplete.',
)
}

return { ok: true, data: ghsaIds }
}

/**
* Discovers GHSA IDs by running coana without applying fixes.
* Returns a list of GHSA IDs, optionally limited.
Expand All @@ -72,7 +133,7 @@ async function discoverGhsaIds(
orgSlug: string,
tarHash: string,
options?: DiscoverGhsaIdsOptions | undefined,
): Promise<string[]> {
): Promise<CResult<string[]>> {
const {
cwd = process.cwd(),
ecosystems,
Expand All @@ -84,12 +145,22 @@ async function discoverGhsaIds(
...options,
} as DiscoverGhsaIdsOptions

// Coana writes a structured JSON result ({ ghsaIds, artifactCount,
// filteredArtifactCount }) via --output-file (available since coana 15.9.7,
// older than the pinned @coana-tech/cli version).
const outputFile = path.join(
os.tmpdir(),
`socket-fix-discovery-${Date.now()}.json`,
)

const foundCResult = await spawnCoanaDlx(
[
'find-vulnerabilities',
cwd,
'--manifests-tar-hash',
tarHash,
'--output-file',
outputFile,
...(ecosystems?.length ? ['--purl-types', ...ecosystems] : []),
...(packageManagers?.length
? ['--package-managers', ...packageManagers]
Expand All @@ -104,16 +175,16 @@ async function discoverGhsaIds(
{ stdio: 'pipe' },
)

if (foundCResult.ok) {
try {
// Coana prints ghsaIds as json-formatted string on the final line of the output.
const ghsaIdsRaw = foundCResult.data.trim().split('\n').pop()
if (ghsaIdsRaw) {
return JSON.parse(ghsaIdsRaw)
}
} catch {}
if (!foundCResult.ok) {
await fs.rm(outputFile, { force: true }).catch(() => {})
return foundCResult
}

try {
return await readStructuredDiscoveryResult(outputFile, silence)
} finally {
await fs.rm(outputFile, { force: true }).catch(() => {})
Comment thread
mtorp marked this conversation as resolved.
}
return []
}

export async function coanaFix(
Expand Down Expand Up @@ -267,16 +338,26 @@ export async function coanaFix(
}

// In local mode, process all discovered/provided IDs (no limit).
const ids: string[] = shouldDiscoverGhsaIds
? await discoverGhsaIds(orgSlug, tarHash, {
coanaVersion,
cwd,
ecosystems,
packageManagers,
silence,
spinner,
})
: ghsas
let ids: string[]
if (shouldDiscoverGhsaIds) {
const discoverCResult = await discoverGhsaIds(orgSlug, tarHash, {
coanaVersion,
cwd,
ecosystems,
packageManagers,
silence,
spinner,
})
if (!discoverCResult.ok) {
if (!silence) {
spinner?.stop()
}
return discoverCResult
}
ids = discoverCResult.data
} else {
ids = ghsas
}

if (ids.length === 0) {
if (!silence) {
Expand Down Expand Up @@ -403,18 +484,25 @@ export async function coanaFix(
let ids: string[] | undefined

if (shouldSpawnCoana) {
ids = (
shouldDiscoverGhsaIds
? await discoverGhsaIds(orgSlug, tarHash, {
coanaVersion,
cwd,
ecosystems,
packageManagers,
silence,
spinner,
})
: ghsas
).slice(0, adjustedPrLimit)
if (shouldDiscoverGhsaIds) {
const discoverCResult = await discoverGhsaIds(orgSlug, tarHash, {
coanaVersion,
cwd,
ecosystems,
packageManagers,
silence,
spinner,
})
if (!discoverCResult.ok) {
if (!silence) {
spinner?.stop()
}
return discoverCResult
}
ids = discoverCResult.data.slice(0, adjustedPrLimit)
} else {
ids = ghsas.slice(0, adjustedPrLimit)
}
}

if (!ids?.length) {
Expand Down
Loading
Loading