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/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index e94226711..f5ab5d8c8 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', @@ -197,6 +204,7 @@ async function run( markdown, org: orgFlag, pullRequest, + reach, readOnly, setAsAlertsPage: pendingHeadFlag, tmp, @@ -213,6 +221,7 @@ async function run( org: string pullRequest: number readOnly: boolean + reach: boolean setAsAlertsPage: boolean tmp: boolean } @@ -413,6 +422,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..579b7fff4 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -7,13 +7,16 @@ 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' -import type { OutputKind } from '../../types.mts' +import type { CResult, OutputKind } from '../../types.mts' export async function handleCreateNewScan({ autoManifest, @@ -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,30 @@ 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, + repoName, + branchName, + outputKind, + interactive, + }) + + if (!reachResult.ok) { + await outputCreateNewScan(reachResult, outputKind, interactive) + return + } + + scanPaths = reachResult.data?.scanPaths || [] + } + const fullScanCResult = await fetchCreateOrgFullScan( - packagePaths, + scanPaths, orgSlug, { commitHash, @@ -153,3 +180,101 @@ export async function handleCreateNewScan({ await outputCreateNewScan(fullScanCResult, outputKind, interactive) } } + +async function performReachabilityAnalysis({ + branchName, + cwd, + orgSlug, + packagePaths, + repoName, +}: { + packagePaths: string[] + orgSlug: string + cwd: string + repoName: string + branchName: string + outputKind: OutputKind + interactive: boolean +}): Promise> { + 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) { + return sockSdkCResult + } + 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) { + return uploadCResult + } + + const tarHash = (uploadCResult.data as { tarHash?: string })?.tarHash + if (!tarHash) { + 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}`) + + // 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', + env: { + ...process.env, + SOCKET_REPO_NAME: repoName, + SOCKET_BRANCH_NAME: branchName, + SOCKET_CLI_VERSION: constants.ENV.INLINED_SOCKET_CLI_VERSION, + }, + }, + ) + + if (!coanaResult.ok) { + return coanaResult + } + + logger.success('Reachability analysis completed successfully') + + // Use the DOT_SOCKET_DOT_FACTS_JSON file for the scan + return { + ok: true, + data: { + 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,