From 52ec6673d3fc2c7ad035ba452865006819291636 Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Tue, 12 Aug 2025 10:30:35 +0200 Subject: [PATCH 1/4] call tier1-reachability-scan/finalize from the Socket CLI --- src/commands/scan/finalize-tier1-scan.mts | 28 ++++++ src/commands/scan/handle-create-new-scan.mts | 24 ++++- src/utils/api.mts | 98 ++++++++++++++++++++ src/utils/coana.mts | 23 ++++- 4 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 src/commands/scan/finalize-tier1-scan.mts diff --git a/src/commands/scan/finalize-tier1-scan.mts b/src/commands/scan/finalize-tier1-scan.mts new file mode 100644 index 000000000..5cfbe5ef9 --- /dev/null +++ b/src/commands/scan/finalize-tier1-scan.mts @@ -0,0 +1,28 @@ +import { sendApiRequest } from '../../utils/api.mts' + +import type { CResult } from '../../types.mts' + +export type FinalizeTier1ScanOptions = { + tier1_reachability_scan_id: string + report_run_id: string +} + +/** + * Finalize a tier1 reachability scan. + * - Associates the tier1 reachability scan metadata with the full scan. + * - Sets the tier1 reachability scan to "finalized" state. + */ +export async function finalizeTier1Scan( + tier1_reachability_scan_id: string, + report_run_id: string, +): Promise> { + // 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. + return await sendApiRequest('tier1-reachability-scan/finalize', { + method: 'POST', + body: { + tier1_reachability_scan_id, + report_run_id, + }, + }) +} diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index 579b7fff4..e0ec7ef90 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -4,12 +4,16 @@ import { pluralize } from '@socketsecurity/registry/lib/words' import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan.mts' import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts' +import { finalizeTier1Scan } from './finalize-tier1-scan.mts' 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 { + extractTier1ReachabilityScanId, + spawnCoana, +} from '../../utils/coana.mts' import { getPackageFilesForScan } from '../../utils/path-resolve.mts' import { setupSdk } from '../../utils/sdk.mts' import { readOrDefaultSocketJson } from '../../utils/socketjson.mts' @@ -112,6 +116,7 @@ export async function handleCreateNewScan({ } let scanPaths: string[] = packagePaths + let tier1ReachabilityScanId: string | undefined // If reachability is enabled, perform reachability analysis if (reach) { @@ -131,6 +136,7 @@ export async function handleCreateNewScan({ } scanPaths = reachResult.data?.scanPaths || [] + tier1ReachabilityScanId = reachResult.data?.tier1ReachabilityScanId } const fullScanCResult = await fetchCreateOrgFullScan( @@ -152,6 +158,15 @@ export async function handleCreateNewScan({ }, ) + if ( + fullScanCResult.ok && + reach && + tier1ReachabilityScanId && + fullScanCResult.data?.id + ) { + await finalizeTier1Scan(tier1ReachabilityScanId, fullScanCResult.data?.id) + } + if (fullScanCResult.ok && report) { if (fullScanCResult.data?.id) { await handleScanReport({ @@ -195,7 +210,9 @@ async function performReachabilityAnalysis({ branchName: string outputKind: OutputKind interactive: boolean -}): Promise> { +}): Promise< + CResult<{ scanPaths?: string[]; tier1ReachabilityScanId: string | undefined }> +> { logger.info('Starting reachability analysis...') packagePaths = packagePaths.filter( @@ -275,6 +292,9 @@ async function performReachabilityAnalysis({ ok: true, data: { scanPaths: [constants.DOT_SOCKET_DOT_FACTS_JSON], + tier1ReachabilityScanId: extractTier1ReachabilityScanId( + constants.DOT_SOCKET_DOT_FACTS_JSON, + ), }, } } diff --git a/src/utils/api.mts b/src/utils/api.mts index 9d77cbe3e..b9e60bcd9 100644 --- a/src/utils/api.mts +++ b/src/utils/api.mts @@ -285,3 +285,101 @@ export async function queryApiSafeJson( } } } + +export async function sendApiRequest( + path: string, + options: { + method: 'POST' | 'PUT' + body?: unknown + fetchSpinnerDesc?: string + }, +): Promise> { + const apiToken = getDefaultToken() + if (!apiToken) { + return { + ok: false, + message: 'Authentication Error', + cause: + 'User must be authenticated to run this command. To log in, run the command `socket login` and enter your Socket API token.', + } + } + + const baseUrl = getDefaultApiBaseUrl() || '' + if (!baseUrl) { + logger.warn( + 'API endpoint is not set and default was empty. Request is likely to fail.', + ) + } + + // Lazily access constants.spinner. + const { spinner } = constants + + if (options.fetchSpinnerDesc) { + spinner.start(`Requesting ${options.fetchSpinnerDesc} from API...`) + } + + let result + try { + result = await fetch( + `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`, + { + method: options.method, + headers: { + Authorization: `Basic ${btoa(`${apiToken}:`)}`, + 'Content-Type': 'application/json', + }, + body: options.body ? JSON.stringify(options.body) : undefined, + }, + ) + if (options.fetchSpinnerDesc) { + spinner.successAndStop( + `Received Socket API response (after requesting ${options.fetchSpinnerDesc}).`, + ) + } + } catch (e) { + if (options.fetchSpinnerDesc) { + spinner.failAndStop( + `An error was thrown while requesting ${options.fetchSpinnerDesc}.`, + ) + } + + const cause = (e as undefined | { message: string })?.message + + debugFn('error', `caught: await fetch() ${options.method} error`) + debugDir('inspect', { error: e }) + + return { + ok: false, + message: 'API Request failed to complete', + ...(cause ? { cause } : {}), + } + } + + if (!result.ok) { + const cause = await getErrorMessageForHttpStatusCode(result.status) + return { + ok: false, + message: 'Socket API returned an error', + cause: `${result.statusText}${cause ? ` (cause: ${cause})` : ''}`, + data: { + code: result.status, + }, + } + } + + try { + const data = await result.json() + return { + ok: true, + data: data as T, + } + } catch (e) { + debugFn('error', 'caught: await result.json() error') + debugDir('inspect', { error: e }) + return { + ok: false, + message: 'API Request failed to complete', + cause: 'There was an unexpected error trying to parse the response JSON', + } + } +} diff --git a/src/utils/coana.mts b/src/utils/coana.mts index 9fe83fbff..a7f799fbf 100644 --- a/src/utils/coana.mts +++ b/src/utils/coana.mts @@ -1,3 +1,5 @@ +import { readFileSync } from 'node:fs' + import { spawn } from '@socketsecurity/registry/lib/spawn' import { getDefaultOrgSlug } from '../commands/ci/fetch-default-org-slug.mts' @@ -14,7 +16,7 @@ export async function spawnCoana( args: string[] | readonly string[], options?: SpawnOptions | undefined, extra?: SpawnExtra | undefined, -): Promise> { +): Promise> { const { env: spawnEnv } = { __proto__: null, ...options } as SpawnOptions const mixinsEnv: Record = { // Lazily access constants.ENV.INLINED_SOCKET_CLI_VERSION. @@ -37,7 +39,8 @@ export async function spawnCoana( // Lazily access constants.nodeMemoryFlags. ...constants.nodeMemoryFlags, // Lazily access constants.coanaBinPath. - constants.coanaBinPath, + // constants.coanaBinPath, + '/Users/martintorp/coana/coana-package-manager/packages/cli/dist/index.js', ...args, ], { @@ -52,6 +55,7 @@ export async function spawnCoana( }, extra, ) + console.log('SPAWNED COANA', output.stdout) return { ok: true, data: output.stdout } } catch (e) { const stderr = (e as any)?.stderr @@ -59,3 +63,18 @@ export async function spawnCoana( return { ok: false, data: e, message } } } + +export function extractTier1ReachabilityScanId( + socketFactsFile: string, +): string | undefined { + try { + console.log('socketFactsFile', socketFactsFile) + const content = readFileSync(socketFactsFile, 'utf8') + console.log('content', content) + const json = JSON.parse(content) + console.log('json', json) + return json.tier1ReachabilityScanId + } catch { + return undefined + } +} From fb789d2354a2606e62dd44a89f2615d9a994d45a Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Tue, 12 Aug 2025 13:36:08 +0200 Subject: [PATCH 2/4] revert some debug stuff --- src/utils/coana.mts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/utils/coana.mts b/src/utils/coana.mts index a7f799fbf..bc6e79d8a 100644 --- a/src/utils/coana.mts +++ b/src/utils/coana.mts @@ -39,8 +39,7 @@ export async function spawnCoana( // Lazily access constants.nodeMemoryFlags. ...constants.nodeMemoryFlags, // Lazily access constants.coanaBinPath. - // constants.coanaBinPath, - '/Users/martintorp/coana/coana-package-manager/packages/cli/dist/index.js', + constants.coanaBinPath, ...args, ], { @@ -55,7 +54,6 @@ export async function spawnCoana( }, extra, ) - console.log('SPAWNED COANA', output.stdout) return { ok: true, data: output.stdout } } catch (e) { const stderr = (e as any)?.stderr From c262572a2045507b3448c12366fd3e8b08f8be2c Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Tue, 12 Aug 2025 13:39:02 +0200 Subject: [PATCH 3/4] remove some debug console.logs --- src/utils/coana.mts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/utils/coana.mts b/src/utils/coana.mts index bc6e79d8a..59528f32d 100644 --- a/src/utils/coana.mts +++ b/src/utils/coana.mts @@ -66,11 +66,8 @@ export function extractTier1ReachabilityScanId( socketFactsFile: string, ): string | undefined { try { - console.log('socketFactsFile', socketFactsFile) const content = readFileSync(socketFactsFile, 'utf8') - console.log('content', content) const json = JSON.parse(content) - console.log('json', json) return json.tier1ReachabilityScanId } catch { return undefined From 9ef71d36fe67b1afe494eb951e5dab86a805265f Mon Sep 17 00:00:00 2001 From: Martin Torp Date: Tue, 12 Aug 2025 13:52:36 +0200 Subject: [PATCH 4/4] fix lint error --- src/utils/api.mts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/utils/api.mts b/src/utils/api.mts index b9e60bcd9..ba522a3bf 100644 --- a/src/utils/api.mts +++ b/src/utils/api.mts @@ -320,16 +320,18 @@ export async function sendApiRequest( let result try { + const fetchOptions = { + method: options.method, + headers: { + Authorization: `Basic ${btoa(`${apiToken}:`)}`, + 'Content-Type': 'application/json', + }, + ...(options.body ? { body: JSON.stringify(options.body) } : {}), + } + result = await fetch( `${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`, - { - method: options.method, - headers: { - Authorization: `Basic ${btoa(`${apiToken}:`)}`, - 'Content-Type': 'application/json', - }, - body: options.body ? JSON.stringify(options.body) : undefined, - }, + fetchOptions, ) if (options.fetchSpinnerDesc) { spinner.successAndStop(