From 67e3648eeb913e22f52d1dfb25d5b27616457362 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Fri, 8 Aug 2025 17:41:14 +0200 Subject: [PATCH 1/3] Add --reach option to run tier 1 as part of scan create --- src/commands/ci/handle-ci.mts | 1 + .../manifest/cmd-manifest-cdxgen.test.mts | 7 +- src/commands/scan/cmd-scan-create.mts | 11 ++ src/commands/scan/create-scan-from-github.mts | 1 + src/commands/scan/handle-create-new-scan.mts | 119 +++++++++++++++++- src/utils/coana.mts | 14 ++- 6 files changed, 145 insertions(+), 8 deletions(-) diff --git a/src/commands/ci/handle-ci.mts b/src/commands/ci/handle-ci.mts index 06f393a9d..1581ce4d1 100644 --- a/src/commands/ci/handle-ci.mts +++ b/src/commands/ci/handle-ci.mts @@ -37,6 +37,7 @@ export async function handleCi(autoManifest: boolean): Promise { // When 'pendingHead' is true, it requires 'branchName' set and 'tmp' false. pendingHead: true, pullRequest: 0, + reach: false, repoName, readOnly: false, report: true, diff --git a/src/commands/manifest/cmd-manifest-cdxgen.test.mts b/src/commands/manifest/cmd-manifest-cdxgen.test.mts index 8ff083298..7bf38b5b6 100644 --- a/src/commands/manifest/cmd-manifest-cdxgen.test.mts +++ b/src/commands/manifest/cmd-manifest-cdxgen.test.mts @@ -15,11 +15,8 @@ describe('socket manifest cdxgen', async () => { // Need to pass it on as env because --config will break cdxgen SOCKET_CLI_CONFIG: '{}', }) - expect(stdout).toMatchInlineSnapshot( - ` - "CycloneDX Generator 11.5.0 - Runtime: Node.js, Version: 24.5.0" - `, + expect(stdout).toMatch( + /^CycloneDX Generator 11\.5\.0\nRuntime: Node\.js, Version: \d+\.\d+\.\d+$/m, ) expect(`\n ${stderr}`).toMatchInlineSnapshot(` " diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index e94226711..83a9872d5 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -93,6 +93,13 @@ const config: CliCommandConfig = { description: 'Similar to --dry-run except it can read from remote, stops before it would create an actual report', }, + reach: { + type: 'boolean', + default: false, + hidden: true, + description: + 'Run tier 1 full application reachability analysis during the scanning process', + }, repo: { type: 'string', shortFlag: 'r', @@ -227,6 +234,9 @@ async function run( repo: string report?: boolean } + const { reach } = cli.flags as { + reach?: boolean + } let [orgSlug] = await determineOrgSlug( String(orgFlag || ''), interactive, @@ -413,6 +423,7 @@ async function run( outputKind, pendingHead: Boolean(pendingHead), pullRequest: Number(pullRequest), + reach: Boolean(reach), readOnly: Boolean(readOnly), repoName, report, diff --git a/src/commands/scan/create-scan-from-github.mts b/src/commands/scan/create-scan-from-github.mts index 71ff400cb..409ab0389 100644 --- a/src/commands/scan/create-scan-from-github.mts +++ b/src/commands/scan/create-scan-from-github.mts @@ -239,6 +239,7 @@ async function scanOneRepo( outputKind, pendingHead: true, pullRequest: 0, + reach: false, readOnly: false, repoName: repoSlug, report: false, diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index a70e68c4f..542224329 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -7,8 +7,11 @@ import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.m import { handleScanReport } from './handle-scan-report.mts' import { outputCreateNewScan } from './output-create-new-scan.mts' import constants from '../../constants.mts' +import { handleApiCall } from '../../utils/api.mts' import { checkCommandInput } from '../../utils/check-input.mts' +import { spawnCoana } from '../../utils/coana.mts' import { getPackageFilesForScan } from '../../utils/path-resolve.mts' +import { setupSdk } from '../../utils/sdk.mts' import { readOrDefaultSocketJson } from '../../utils/socketjson.mts' import { detectManifestActions } from '../manifest/detect-manifest-actions.mts' import { generateAutoManifest } from '../manifest/generate_auto_manifest.mts' @@ -28,6 +31,7 @@ export async function handleCreateNewScan({ outputKind, pendingHead, pullRequest, + reach, readOnly, repoName, report, @@ -46,6 +50,7 @@ export async function handleCreateNewScan({ pendingHead: boolean pullRequest: number outputKind: OutputKind + reach: boolean readOnly: boolean repoName: string report: boolean @@ -106,8 +111,27 @@ export async function handleCreateNewScan({ return } + let scanPaths: string[] = packagePaths + + // If reachability is enabled, perform reachability analysis + if (reach) { + const reachResult = await performReachabilityAnalysis({ + packagePaths, + orgSlug, + cwd, + outputKind, + interactive, + }) + + if (!reachResult.ok || !reachResult.scanPaths) { + return + } + + scanPaths = reachResult.scanPaths + } + const fullScanCResult = await fetchCreateOrgFullScan( - packagePaths, + scanPaths, orgSlug, { commitHash, @@ -153,3 +177,96 @@ export async function handleCreateNewScan({ await outputCreateNewScan(fullScanCResult, outputKind, interactive) } } + +async function performReachabilityAnalysis({ + cwd, + interactive, + orgSlug, + outputKind, + packagePaths, +}: { + packagePaths: string[] + orgSlug: string + cwd: string + outputKind: OutputKind + interactive: boolean +}): Promise<{ ok: boolean; scanPaths?: string[] }> { + logger.info('Starting reachability analysis...') + + packagePaths = packagePaths.filter( + p => + /* Exclude DOT_SOCKET_DOT_FACTS_JSON from previous runs */ !p.includes( + constants.DOT_SOCKET_DOT_FACTS_JSON, + ), + ) + + // Lazily access constants.spinner. + const { spinner } = constants + + // Setup SDK for uploading manifests + const sockSdkCResult = await setupSdk() + if (!sockSdkCResult.ok) { + await outputCreateNewScan(sockSdkCResult, outputKind, interactive) + return { ok: false } + } + const sockSdk = sockSdkCResult.data + + // Upload manifests to get tar hash + spinner.start('Uploading manifests for reachability analysis...') + const uploadCResult = await handleApiCall( + sockSdk.uploadManifestFiles(orgSlug, packagePaths), + { desc: 'upload manifests' }, + ) + spinner.stop() + + if (!uploadCResult.ok) { + await outputCreateNewScan(uploadCResult, outputKind, interactive) + return { ok: false } + } + + const tarHash = (uploadCResult.data as { tarHash?: string })?.tarHash + if (!tarHash) { + await outputCreateNewScan( + { + ok: false, + message: 'Failed to get manifest tar hash', + cause: 'Server did not return a tar hash for the uploaded manifests', + }, + outputKind, + interactive, + ) + return { ok: false } + } + + logger.success(`Manifests uploaded successfully. Tar hash: ${tarHash}`) + + // Run Coana with the manifests tar hash + logger.info('Running reachability analysis with Coana...') + const coanaResult = await spawnCoana( + [ + 'run', + cwd, + '--output-dir', + cwd, + '--socket-mode', + constants.DOT_SOCKET_DOT_FACTS_JSON, + '--disable-report-submission', + '--manifests-tar-hash', + tarHash, + ], + { cwd, stdio: 'inherit' }, + ) + + if (!coanaResult.ok) { + await outputCreateNewScan(coanaResult, outputKind, interactive) + return { ok: false } + } + + logger.success('Reachability analysis completed successfully') + + // Use the DOT_SOCKET_DOT_FACTS_JSON file for the scan + return { + ok: true, + scanPaths: [constants.DOT_SOCKET_DOT_FACTS_JSON], + } +} diff --git a/src/utils/coana.mts b/src/utils/coana.mts index 1f4a73960..fe0643ed4 100644 --- a/src/utils/coana.mts +++ b/src/utils/coana.mts @@ -1,8 +1,8 @@ import { spawn } from '@socketsecurity/registry/lib/spawn' +import { getDefaultOrgSlug } from '../commands/ci/fetch-default-org-slug.mts' import constants from '../constants.mts' import { getDefaultToken } from './sdk.mts' -import { getDefaultOrgSlug } from '../commands/ci/fetch-default-org-slug.mts' import type { CResult } from '../types.mts' import type { @@ -15,10 +15,20 @@ export async function spawnCoana( options?: SpawnOptions | undefined, extra?: SpawnExtra | undefined, ): Promise> { - const { env: spawnEnv } = { __proto__: null, ...options } as SpawnOptions + const { + env: spawnEnv, + spinner, + stdio, + } = { __proto__: null, ...options } as SpawnOptions const orgSlugCResult = await getDefaultOrgSlug() const SOCKET_CLI_API_TOKEN = getDefaultToken() const SOCKET_ORG_SLUG = orgSlugCResult.ok ? orgSlugCResult.data : undefined + + // Stop spinner before streaming output if stdio is 'inherit' + if (stdio === 'inherit' && spinner) { + spinner.stop() + } + try { const output = await spawn( constants.execPath, From ee0199de67c6f610d5e70f2a49613dc4d53d57bd Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Mon, 11 Aug 2025 10:07:03 +0200 Subject: [PATCH 2/3] pass SOCKET_REPO_NAME, SOCKET_BRANCH_NAME and SOCKET_CLI_VERSION to Coana CLI --- src/commands/scan/handle-create-new-scan.mts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index 542224329..6a7f30558 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -119,6 +119,8 @@ export async function handleCreateNewScan({ packagePaths, orgSlug, cwd, + repoName, + branchName, outputKind, interactive, }) @@ -179,15 +181,19 @@ export async function handleCreateNewScan({ } async function performReachabilityAnalysis({ + branchName, cwd, interactive, orgSlug, outputKind, packagePaths, + repoName, }: { packagePaths: string[] orgSlug: string cwd: string + repoName: string + branchName: string outputKind: OutputKind interactive: boolean }): Promise<{ ok: boolean; scanPaths?: string[] }> { @@ -254,7 +260,16 @@ async function performReachabilityAnalysis({ '--manifests-tar-hash', tarHash, ], - { cwd, stdio: 'inherit' }, + { + cwd, + stdio: 'inherit', + env: { + ...process.env, + SOCKET_REPO_NAME: repoName, + SOCKET_BRANCH_NAME: branchName, + SOCKET_CLI_VERSION: constants.ENV.INLINED_SOCKET_CLI_VERSION, + }, + }, ) if (!coanaResult.ok) { From 9d771f2a6d8f98989558683e9715f56860e8650f Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Mon, 11 Aug 2025 11:27:10 +0200 Subject: [PATCH 3/3] update performReachabilityAnalysis to return CResult --- src/commands/scan/cmd-scan-create.mts | 5 +-- src/commands/scan/handle-create-new-scan.mts | 39 ++++++++------------ 2 files changed, 18 insertions(+), 26 deletions(-) diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 83a9872d5..f5ab5d8c8 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -204,6 +204,7 @@ async function run( markdown, org: orgFlag, pullRequest, + reach, readOnly, setAsAlertsPage: pendingHeadFlag, tmp, @@ -220,6 +221,7 @@ async function run( org: string pullRequest: number readOnly: boolean + reach: boolean setAsAlertsPage: boolean tmp: boolean } @@ -234,9 +236,6 @@ async function run( repo: string report?: boolean } - const { reach } = cli.flags as { - reach?: boolean - } let [orgSlug] = await determineOrgSlug( String(orgFlag || ''), interactive, diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index 6a7f30558..579b7fff4 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -16,7 +16,7 @@ import { readOrDefaultSocketJson } from '../../utils/socketjson.mts' import { detectManifestActions } from '../manifest/detect-manifest-actions.mts' import { generateAutoManifest } from '../manifest/generate_auto_manifest.mts' -import type { OutputKind } from '../../types.mts' +import type { CResult, OutputKind } from '../../types.mts' export async function handleCreateNewScan({ autoManifest, @@ -125,11 +125,12 @@ export async function handleCreateNewScan({ interactive, }) - if (!reachResult.ok || !reachResult.scanPaths) { + if (!reachResult.ok) { + await outputCreateNewScan(reachResult, outputKind, interactive) return } - scanPaths = reachResult.scanPaths + scanPaths = reachResult.data?.scanPaths || [] } const fullScanCResult = await fetchCreateOrgFullScan( @@ -183,9 +184,7 @@ export async function handleCreateNewScan({ async function performReachabilityAnalysis({ branchName, cwd, - interactive, orgSlug, - outputKind, packagePaths, repoName, }: { @@ -196,7 +195,7 @@ async function performReachabilityAnalysis({ branchName: string outputKind: OutputKind interactive: boolean -}): Promise<{ ok: boolean; scanPaths?: string[] }> { +}): Promise> { logger.info('Starting reachability analysis...') packagePaths = packagePaths.filter( @@ -212,8 +211,7 @@ async function performReachabilityAnalysis({ // Setup SDK for uploading manifests const sockSdkCResult = await setupSdk() if (!sockSdkCResult.ok) { - await outputCreateNewScan(sockSdkCResult, outputKind, interactive) - return { ok: false } + return sockSdkCResult } const sockSdk = sockSdkCResult.data @@ -226,22 +224,16 @@ async function performReachabilityAnalysis({ spinner.stop() if (!uploadCResult.ok) { - await outputCreateNewScan(uploadCResult, outputKind, interactive) - return { ok: false } + return uploadCResult } const tarHash = (uploadCResult.data as { tarHash?: string })?.tarHash if (!tarHash) { - await outputCreateNewScan( - { - ok: false, - message: 'Failed to get manifest tar hash', - cause: 'Server did not return a tar hash for the uploaded manifests', - }, - outputKind, - interactive, - ) - return { ok: false } + return { + ok: false, + message: 'Failed to get manifest tar hash', + cause: 'Server did not return a tar hash for the uploaded manifests', + } } logger.success(`Manifests uploaded successfully. Tar hash: ${tarHash}`) @@ -273,8 +265,7 @@ async function performReachabilityAnalysis({ ) if (!coanaResult.ok) { - await outputCreateNewScan(coanaResult, outputKind, interactive) - return { ok: false } + return coanaResult } logger.success('Reachability analysis completed successfully') @@ -282,6 +273,8 @@ async function performReachabilityAnalysis({ // Use the DOT_SOCKET_DOT_FACTS_JSON file for the scan return { ok: true, - scanPaths: [constants.DOT_SOCKET_DOT_FACTS_JSON], + data: { + scanPaths: [constants.DOT_SOCKET_DOT_FACTS_JSON], + }, } }