diff --git a/CHANGELOG.md b/CHANGELOG.md index 81e8a78ad8..0f881e27de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] ### Changed -- Updated the Coana CLI to v `15.9.7`. +- Updated the Coana CLI to v `15.9.9`. - `socket scan view` now reads completed scans from Socket's cached immutable results, retrying briefly while a fresh scan finalizes; `--stream` keeps streaming live results. - `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!". diff --git a/package.json b/package.json index 35f92a99b2..2c1eec0128 100644 --- a/package.json +++ b/package.json @@ -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.7", + "@coana-tech/cli": "15.9.9", "@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 fb1e6702d8..b79bff0528 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -132,8 +132,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.9.7 - version: 15.9.7 + specifier: 15.9.9 + version: 15.9.9 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -806,8 +806,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.9.7': - resolution: {integrity: sha512-1CGis51nRr3Sl3uKYLd7/c4L+vEtYlJUiGYh1TL58WtD4Ojd+qaticSFDa+mywbKLRBXpAPwPWBOes0OQZNP1g==} + '@coana-tech/cli@15.9.9': + resolution: {integrity: sha512-LfJSd0CagP/Jvu+OKodH+fSwf74sEVr3N6m4dYvr+jrRyOrhutrWMN6g517PwL6eioAGKTT39F0SWlQO/t0crg==} hasBin: true '@colors/colors@1.5.0': @@ -5509,7 +5509,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.9.7': {} + '@coana-tech/cli@15.9.9': {} '@colors/colors@1.5.0': optional: true diff --git a/src/commands/ci/handle-ci.mts b/src/commands/ci/handle-ci.mts index d513b86dd8..776e3561e1 100644 --- a/src/commands/ci/handle-ci.mts +++ b/src/commands/ci/handle-ci.mts @@ -51,6 +51,7 @@ export async function handleCi(autoManifest: boolean): Promise { pendingHead: true, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '', reachAnalysisTimeout: '', diff --git a/src/commands/manifest/scripts/assemble.mts b/src/commands/manifest/scripts/assemble.mts index 1d47494055..bce869906e 100644 --- a/src/commands/manifest/scripts/assemble.mts +++ b/src/commands/manifest/scripts/assemble.mts @@ -379,8 +379,8 @@ function buildArtifactPaths( } coords.add(coordKey) const pi = projectsByGav.get(gav(c.group, c.name, c.version ?? '')) - const sources = (pi?.sources ?? []).filter(fileExists) - const targets = [...new Set([...fn.targets, ...(pi?.targets ?? [])])] + const sources = (pi?.sources ?? []).filter(fileExists).sort() + const targets = [...new Set(pi ? pi.targets : fn.targets)] .filter(fileExists) .sort() if (sources.length) { diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 3de7991f44..a7ff50d501 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -241,6 +241,7 @@ async function run( committers, cwd: cwdOverride, defaultBranch, + dynamicSbomInference, interactive = true, json, markdown, @@ -275,6 +276,7 @@ async function run( committers: string cwd: string defaultBranch: boolean + dynamicSbomInference: boolean interactive: boolean json: boolean markdown: boolean @@ -353,6 +355,11 @@ async function run( autoManifest = false } } + // --dynamic-sbom-inference requires auto-manifest to generate the + // per-workspace facts it feeds to Coana. + if (dynamicSbomInference) { + autoManifest = true + } if (!branchName) { if (sockJson.defaults?.scan?.create?.branch) { branchName = sockJson.defaults.scan.create.branch @@ -505,6 +512,7 @@ async function run( reachVersion !== reachabilityFlags['reachVersion']?.default const isUsingAnyReachabilityFlags = + dynamicSbomInference || hasReachEcosystems || hasReachExcludePaths || isUsingNonDefaultAnalytics || @@ -625,6 +633,7 @@ async function run( pendingHead: Boolean(pendingHead), pullRequest: Number(pullRequest), reach: { + dynamicSbomInference: Boolean(dynamicSbomInference), excludePaths, reachAnalysisMemoryLimit, reachAnalysisTimeout, diff --git a/src/commands/scan/cmd-scan-reach.mts b/src/commands/scan/cmd-scan-reach.mts index 1c11faddb8..42f910be72 100644 --- a/src/commands/scan/cmd-scan-reach.mts +++ b/src/commands/scan/cmd-scan-reach.mts @@ -267,6 +267,9 @@ async function run( outputKind, outputPath: outputPath || '', reachabilityOptions: { + // Not exposed here: it relies on --auto-manifest generating per-workspace + // Socket facts first, which `socket scan reach` never runs. + dynamicSbomInference: false, excludePaths, reachAnalysisMemoryLimit, reachAnalysisTimeout, diff --git a/src/commands/scan/create-scan-from-github.mts b/src/commands/scan/create-scan-from-github.mts index 47537cf53c..994514089d 100644 --- a/src/commands/scan/create-scan-from-github.mts +++ b/src/commands/scan/create-scan-from-github.mts @@ -253,6 +253,7 @@ async function scanOneRepo( pendingHead: true, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '', reachAnalysisTimeout: '', diff --git a/src/commands/scan/exclude-paths.test.mts b/src/commands/scan/exclude-paths.test.mts index 23e45cab46..5865271074 100644 --- a/src/commands/scan/exclude-paths.test.mts +++ b/src/commands/scan/exclude-paths.test.mts @@ -14,6 +14,7 @@ function makeReachOptions( overrides: Partial = {}, ): ReachabilityOptions { return { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', diff --git a/src/commands/scan/handle-create-new-scan.test.mts b/src/commands/scan/handle-create-new-scan.test.mts index 633daba56c..2c1cc624ce 100644 --- a/src/commands/scan/handle-create-new-scan.test.mts +++ b/src/commands/scan/handle-create-new-scan.test.mts @@ -86,6 +86,7 @@ function createConfig( pendingHead: false, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -198,6 +199,7 @@ describe('handleCreateNewScan excludePaths', () => { pendingHead: false, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: ['tests', 'packages/*'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -261,6 +263,7 @@ describe('handleCreateNewScan excludePaths', () => { pendingHead: false, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: ['apps/api/tests', '**/dist'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -330,6 +333,7 @@ describe('handleCreateNewScan excludePaths', () => { pendingHead: false, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: ['tests'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -389,6 +393,7 @@ describe('handleCreateNewScan excludePaths', () => { pendingHead: false, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: ['apps/api'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -448,6 +453,7 @@ describe('handleCreateNewScan excludePaths', () => { pendingHead: false, pullRequest: 0, reach: { + dynamicSbomInference: false, excludePaths: ['tests'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', diff --git a/src/commands/scan/handle-scan-reach.test.mts b/src/commands/scan/handle-scan-reach.test.mts index c3252a75c6..2c780e0b17 100644 --- a/src/commands/scan/handle-scan-reach.test.mts +++ b/src/commands/scan/handle-scan-reach.test.mts @@ -104,6 +104,7 @@ describe('handleScanReach', () => { it('applies excludePaths to manifest discovery and reachability analysis', async () => { const reachabilityOptions = { + dynamicSbomInference: false, excludePaths: ['tests', 'packages/*'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -156,6 +157,7 @@ describe('handleScanReach', () => { it('translates excludePaths from the scan root for nested targets', async () => { const reachabilityOptions = { + dynamicSbomInference: false, excludePaths: ['apps/api/tests', '**/dist'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -218,6 +220,7 @@ describe('handleScanReach', () => { checks.every(check => check.test), ) const reachabilityOptions = { + dynamicSbomInference: false, excludePaths: ['apps/api'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -266,6 +269,7 @@ describe('handleScanReach', () => { mockFindSocketYmlSync.mockReturnValueOnce({ ok: false }) const reachabilityOptions = { + dynamicSbomInference: false, excludePaths: ['tests'], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -318,6 +322,7 @@ describe('handleScanReach', () => { }, }) const reachabilityOptions = { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -355,6 +360,7 @@ describe('handleScanReach', () => { it('does not call finalize when Coana did not return a full application reachability scan id', async () => { const reachabilityOptions = { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', @@ -405,6 +411,7 @@ describe('handleScanReach', () => { cause: 'Socket API server error (503)', }) const reachabilityOptions = { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '8192', reachAnalysisTimeout: '', diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 69e7973a9c..4db3a2ad40 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -22,6 +22,7 @@ import type { Spinner } from '@socketsecurity/registry/lib/spinner' import type { StdioOptions } from 'node:child_process' export type ReachabilityOptions = { + dynamicSbomInference: boolean excludePaths: string[] reachAnalysisMemoryLimit: string reachAnalysisTimeout: string @@ -251,6 +252,9 @@ export async function performReachabilityAnalysis( ...(reachabilityOptions.reachExcludePaths.length ? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths] : []), + ...(reachabilityOptions.dynamicSbomInference + ? ['--maven-use-only-root-socket-facts'] + : []), ...(reachabilityOptions.reachLazyMode ? ['--lazy-mode'] : []), ...(reachabilityOptions.reachSkipCache ? ['--skip-cache-usage'] : []), ...(reachabilityOptions.reachUseOnlyPregeneratedSboms diff --git a/src/commands/scan/perform-reachability-analysis.test.mts b/src/commands/scan/perform-reachability-analysis.test.mts index 4e940ae789..773dfc3015 100644 --- a/src/commands/scan/perform-reachability-analysis.test.mts +++ b/src/commands/scan/perform-reachability-analysis.test.mts @@ -75,6 +75,7 @@ vi.mock('@socketsecurity/registry/lib/logger', () => ({ function makeReachabilityOptions(): ReachabilityOptions { return { + dynamicSbomInference: false, excludePaths: [], reachAnalysisMemoryLimit: '', reachAnalysisTimeout: '', diff --git a/src/commands/scan/reachability-flags.mts b/src/commands/scan/reachability-flags.mts index 9f4dc6e891..8fcea365bc 100644 --- a/src/commands/scan/reachability-flags.mts +++ b/src/commands/scan/reachability-flags.mts @@ -4,6 +4,13 @@ import { getReachabilityEcosystemChoices } from '../../utils/ecosystem.mts' import type { MeowFlags } from '../../flags.mts' export const reachabilityFlags: MeowFlags = { + dynamicSbomInference: { + type: 'boolean', + default: false, + hidden: true, + description: + 'Internal: enables dynamic SBOM inference for full application reachability analysis. Passes --maven-use-only-root-socket-facts to Coana and implies --auto-manifest.', + }, reachVersion: { type: 'string', description: `Override the version of @coana-tech/cli used for reachability analysis. Default: ${constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION}.`,