Skip to content
Open
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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@ 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]
## [1.1.113](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.113) - 2026-06-03

### Added
- **`socket manifest bazel [beta]`** — Generate Bazel JVM SBOM manifests by running `bazel query` against discovered Maven repos in a Bazel workspace. Closes the inline-Maven-declaration gap that lockfile-only parsing misses for repos like envoy, ray, tensorflow, tink-java, and or-tools. Auto-detects Bzlmod and legacy `WORKSPACE`.
- **`socket scan create --auto-manifest`** now covers Bazel workspaces in addition to Gradle/Scala/Kotlin/Conda. Repos with `MODULE.bazel`, `WORKSPACE`, or `WORKSPACE.bazel` are detected automatically and their Maven dependencies extracted as part of the standard scan-create flow.
- **Bazel PyPI extraction** — `socket manifest bazel --ecosystem pypi` now generates `requirements.txt` for Python Bazel workspaces. Discovers custom `rules_python` pip hub names with Bazel command output first, queries `py_library` / `py_binary` / `py_test` dependencies, resolves canonical pinned versions from `requirements_lock.txt`, and emits PEP 503-normalized `name==version` lines. Supports both Bzlmod (`pip.parse`) and legacy `WORKSPACE` (`pip_parse` / `pip_install`) configurations. PyPI remains explicit opt-in for `socket scan create --auto-manifest` until real-world no-lockfile recovery is validated.

### Changed
- **Bazel diagnostics** — `socket manifest bazel --verbose` now emits bounded subprocess traces with argv, cwd, duration, exit status, output sizes, and failure stderr tails to make customer log-only triage safer and faster.
- Updated the Coana CLI to v `15.3.20`.

## [1.1.112](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.112) - 2026-05-29

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.112",
"version": "1.1.113",
"description": "CLI for Socket.dev",
"homepage": "https://github.com/SocketDev/socket-cli",
"license": "MIT AND OFL-1.1",
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.15",
"@coana-tech/cli": "15.3.20",
"@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.

10 changes: 6 additions & 4 deletions src/commands/scan/finalize-tier1-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,19 @@ import type { CResult } from '../../types.mts'

export type FinalizeTier1ScanOptions = {
tier1_reachability_scan_id: string
report_run_id: string
report_run_id: string | null
}

/**
* Finalize a tier1 reachability scan.
* - Associates the tier1 reachability scan metadata with the full scan.
* - Sets the tier1 reachability scan to "finalized" state.
* - Associates the tier1 reachability scan metadata with the full scan
* (or with `null` when called from a standalone reachability flow that
* has no full scan to bind to).
* - Transitions the tier1 reachability scan to its DONE terminal state.
*/
export async function finalizeTier1Scan(
tier1ReachabilityScanId: string,
scanId: string,
scanId: string | null,
): Promise<CResult<unknown>> {
// we do not use the SDK here because the tier1-reachability-scan/finalize is a hidden
// endpoint that is not part of the OpenAPI specification.
Expand Down
17 changes: 17 additions & 0 deletions src/commands/scan/handle-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { pluralize } from '@socketsecurity/registry/lib/words'

import { applyFullExcludePaths } from './exclude-paths.mts'
import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts'
import { finalizeTier1Scan } from './finalize-tier1-scan.mts'
import { outputScanReach } from './output-scan-reach.mts'
import { performReachabilityAnalysis } from './perform-reachability-analysis.mts'
import constants from '../../constants.mts'
Expand Down Expand Up @@ -104,5 +105,21 @@ export async function handleScanReach({

spinner.stop()

// Standalone reachability has no full scan to bind to, but the tier1
// reachability scan row still needs to transition to its DONE terminal
// state — otherwise it sits at the post-Coana intermediate state forever
// and looks indistinguishable from a stuck run. Pass `null` as the full
// scan id; the endpoint accepts it for this flow. Best-effort: never
// block the user-visible output on this.
const tier1Id = result.ok ? result.data?.tier1ReachabilityScanId : undefined
if (tier1Id) {
const finalizeResult = await finalizeTier1Scan(tier1Id, null)
if (!finalizeResult.ok) {
logger.warn(
`Failed to finalize tier1 reachability scan: ${finalizeResult.message}${finalizeResult.cause ? ` — ${finalizeResult.cause}` : ''}`,
)
}
}

await outputScanReach(result, { cwd, outputKind, outputPath })
}
157 changes: 156 additions & 1 deletion src/commands/scan/handle-scan-reach.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,22 @@ import { handleScanReach } from './handle-scan-reach.mts'
const {
mockCheckCommandInput,
mockFetchSupportedScanFileNames,
mockFinalizeTier1Scan,
mockFindSocketYmlSync,
mockGetPackageFilesForScan,
mockLoggerSuccess,
mockLoggerWarn,
mockOutputScanReach,
mockPerformReachabilityAnalysis,
mockSentryInternalsSymbol,
} = vi.hoisted(() => ({
mockCheckCommandInput: vi.fn(),
mockFetchSupportedScanFileNames: vi.fn(),
mockFinalizeTier1Scan: vi.fn(),
mockFindSocketYmlSync: vi.fn(),
mockGetPackageFilesForScan: vi.fn(),
mockLoggerSuccess: vi.fn(),
mockLoggerWarn: vi.fn(),
mockOutputScanReach: vi.fn(),
mockPerformReachabilityAnalysis: vi.fn(),
mockSentryInternalsSymbol: Symbol('kInternalsSymbol'),
Expand All @@ -24,6 +30,10 @@ vi.mock('./fetch-supported-scan-file-names.mts', () => ({
fetchSupportedScanFileNames: mockFetchSupportedScanFileNames,
}))

vi.mock('./finalize-tier1-scan.mts', () => ({
finalizeTier1Scan: mockFinalizeTier1Scan,
}))

vi.mock('./output-scan-reach.mts', () => ({
outputScanReach: mockOutputScanReach,
}))
Expand Down Expand Up @@ -64,7 +74,8 @@ vi.mock('../../utils/path-resolve.mts', () => ({

vi.mock('@socketsecurity/registry/lib/logger', () => ({
logger: {
success: vi.fn(),
success: mockLoggerSuccess,
warn: mockLoggerWarn,
},
}))

Expand All @@ -76,6 +87,7 @@ describe('handleScanReach', () => {
ok: true,
data: { npm: { packageJson: { pattern: 'package.json' } } },
})
mockFinalizeTier1Scan.mockResolvedValue({ data: undefined, ok: true })
mockFindSocketYmlSync.mockReturnValue({
ok: true,
data: { parsed: { projectIgnorePaths: ['vendor/**'] } },
Expand Down Expand Up @@ -292,4 +304,147 @@ describe('handleScanReach', () => {
},
)
})

it('finalizes the tier1 reachability scan with a null report_run_id when Coana returned a scan id', async () => {
mockPerformReachabilityAnalysis.mockResolvedValueOnce({
ok: true,
data: {
reachabilityReport: '.socket.facts.json',
tier1ReachabilityScanId: 'tier1-id',
},
})
const reachabilityOptions = {
excludePaths: [],
reachAnalysisMemoryLimit: 8192,
reachAnalysisTimeout: 0,
reachConcurrency: 1,
reachContinueOnAnalysisErrors: false,
reachContinueOnInstallErrors: false,
reachContinueOnMissingLockFiles: false,
reachContinueOnNoSourceFiles: false,
reachDebug: false,
reachDetailedAnalysisLogFile: false,
reachDisableAnalytics: false,
reachDisableExternalToolChecks: false,
reachEcosystems: [],
reachEnableAnalysisSplitting: false,
reachExcludePaths: [],
reachLazyMode: false,
reachSkipCache: false,
reachUseOnlyPregeneratedSboms: false,
reachVersion: undefined,
}

await handleScanReach({
cwd: '/repo',
interactive: false,
orgSlug: 'fakeOrg',
outputKind: 'text',
outputPath: '',
reachabilityOptions,
targets: ['.'],
})

expect(mockFinalizeTier1Scan).toHaveBeenCalledWith('tier1-id', null)
})

it('does not call finalize when Coana did not return a tier1 reachability scan id', async () => {
const reachabilityOptions = {
excludePaths: [],
reachAnalysisMemoryLimit: 8192,
reachAnalysisTimeout: 0,
reachConcurrency: 1,
reachContinueOnAnalysisErrors: false,
reachContinueOnInstallErrors: false,
reachContinueOnMissingLockFiles: false,
reachContinueOnNoSourceFiles: false,
reachDebug: false,
reachDetailedAnalysisLogFile: false,
reachDisableAnalytics: false,
reachDisableExternalToolChecks: false,
reachEcosystems: [],
reachEnableAnalysisSplitting: false,
reachExcludePaths: [],
reachLazyMode: false,
reachSkipCache: false,
reachUseOnlyPregeneratedSboms: false,
reachVersion: undefined,
}

await handleScanReach({
cwd: '/repo',
interactive: false,
orgSlug: 'fakeOrg',
outputKind: 'text',
outputPath: '',
reachabilityOptions,
targets: ['.'],
})

expect(mockFinalizeTier1Scan).not.toHaveBeenCalled()
})

it('warns but still produces scan output when tier1 finalize fails', async () => {
mockPerformReachabilityAnalysis.mockResolvedValueOnce({
ok: true,
data: {
reachabilityReport: '.socket.facts.json',
tier1ReachabilityScanId: 'tier1-id',
},
})
// Finalize fails with the CResult error shape; the command must not abort.
mockFinalizeTier1Scan.mockResolvedValueOnce({
ok: false,
message: 'Finalize request failed',
cause: 'Socket API server error (503)',
})
const reachabilityOptions = {
excludePaths: [],
reachAnalysisMemoryLimit: 8192,
reachAnalysisTimeout: 0,
reachConcurrency: 1,
reachContinueOnAnalysisErrors: false,
reachContinueOnInstallErrors: false,
reachContinueOnMissingLockFiles: false,
reachContinueOnNoSourceFiles: false,
reachDebug: false,
reachDetailedAnalysisLogFile: false,
reachDisableAnalytics: false,
reachDisableExternalToolChecks: false,
reachEcosystems: [],
reachEnableAnalysisSplitting: false,
reachExcludePaths: [],
reachLazyMode: false,
reachSkipCache: false,
reachUseOnlyPregeneratedSboms: false,
reachVersion: undefined,
}

// The handler resolves normally (no throw, returns undefined) so the
// command proceeds and exits 0 rather than being blocked by the failure.
await expect(
handleScanReach({
cwd: '/repo',
interactive: false,
orgSlug: 'fakeOrg',
outputKind: 'text',
outputPath: '',
reachabilityOptions,
targets: ['.'],
}),
).resolves.toBeUndefined()

expect(mockFinalizeTier1Scan).toHaveBeenCalledWith('tier1-id', null)
// The failure is surfaced as a single warning carrying message and cause.
expect(mockLoggerWarn).toHaveBeenCalledTimes(1)
const { 0: warnMessage } = mockLoggerWarn.mock.calls[0]
expect(warnMessage).toContain('Failed to finalize tier1 reachability scan')
expect(warnMessage).toContain('Finalize request failed')
expect(warnMessage).toContain('Socket API server error (503)')
// Normal scan output is still produced; the command is not blocked.
expect(mockOutputScanReach).toHaveBeenCalledWith(
expect.objectContaining({ ok: true }),
{ cwd: '/repo', outputKind: 'text', outputPath: '' },
)
})
})
Loading