diff --git a/package.json b/package.json index daf45d6acb..36b219555d 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,7 @@ "@socketregistry/packageurl-js": "1.0.9", "@socketsecurity/config": "3.0.1", "@socketsecurity/registry": "1.1.17", - "@socketsecurity/sdk": "1.4.96", + "@socketsecurity/sdk": "4.0.3", "@types/blessed": "0.1.25", "@types/cmd-shim": "5.0.2", "@types/js-yaml": "4.0.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e989dabe2c..0bd73decd0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -216,8 +216,8 @@ importers: specifier: 1.1.17 version: 1.1.17 '@socketsecurity/sdk': - specifier: 1.4.96 - version: 1.4.96 + specifier: 4.0.3 + version: 4.0.3 '@types/blessed': specifier: 0.1.25 version: 0.1.25 @@ -1825,9 +1825,9 @@ packages: resolution: {integrity: sha512-5j0eH6JaBZlcvnbdu+58Sw8c99AK25PTp0Z/lwP7HknHdJ0TMMoTzNIBbp7WCTZKoGrPgBWchi0udN1ObZ53VQ==} engines: {node: '>=18'} - '@socketsecurity/sdk@1.4.96': - resolution: {integrity: sha512-yZn50KLeCerKQRYTUfMzB5m7mraH1Rhr01WDLgzgaw3Do3qEsHy7sdoHI5o1FvyIWd8ebBspK3PJ2xqyrdQejw==} - engines: {node: '>=18'} + '@socketsecurity/sdk@4.0.3': + resolution: {integrity: sha512-m/YswFjERxa9nkw4PTGw/8Q6HDRNuvYl5HLWHTtDTj7TLLsk2efbgjpzarqouql2+1VdukDzt5YRejJq7fptXg==} + engines: {node: '>=18.20.8', npm: '>=12.0.0', pnpm: '>=11.0.5'} '@socketsecurity/socket-patch-android-arm64@2.0.0': resolution: {integrity: sha512-R/xsoKr0pRpxHeqoIxMj7R4w77mBPQ5Asv4PpNg2UTIEhxUx9XyXW7MVq9XjjrnWIEMpV1TwOptjvBicFTZZAQ==} @@ -6510,9 +6510,7 @@ snapshots: '@socketsecurity/registry@1.1.17': {} - '@socketsecurity/sdk@1.4.96': - dependencies: - '@socketsecurity/registry': 1.1.17 + '@socketsecurity/sdk@4.0.3': {} '@socketsecurity/socket-patch-android-arm64@2.0.0': optional: true diff --git a/src/commands/audit-log/output-audit-log.mts b/src/commands/audit-log/output-audit-log.mts index f2cc986d7d..7a07d2a044 100644 --- a/src/commands/audit-log/output-audit-log.mts +++ b/src/commands/audit-log/output-audit-log.mts @@ -120,25 +120,29 @@ export async function outputAsJson( org: orgSlug, page, perPage, - logs: auditLogs.data.results.map(log => { - // Note: The subset is pretty arbitrary - const { - created_at, - event_id, - ip_address, - type, - user_agent, - user_email, - } = log - return { - event_id, - created_at, - ip_address, - type, - user_agent, - user_email, - } - }), + logs: auditLogs.data.results.map( + ( + log: SocketSdkSuccessResult<'getAuditLogEvents'>['data']['results'][number], + ) => { + // Note: The subset is pretty arbitrary + const { + created_at, + event_id, + ip_address, + type, + user_agent, + user_email, + } = log + return { + event_id, + created_at, + ip_address, + type, + user_agent, + user_email, + } + }, + ), }, }) } @@ -196,14 +200,18 @@ async function outputWithBlessed( orgSlug: string, ) { const filteredLogs = data.results - const formattedOutput = filteredLogs.map(logs => [ - logs.event_id ?? '', - msAtHome(logs.created_at ?? ''), - logs.type ?? '', - logs.user_email ?? '', - logs.ip_address ?? '', - logs.user_agent ?? '', - ]) + const formattedOutput: string[][] = filteredLogs.map( + ( + logs: SocketSdkSuccessResult<'getAuditLogEvents'>['data']['results'][number], + ) => [ + logs.event_id ?? '', + msAtHome(logs.created_at ?? ''), + logs.type ?? '', + logs.user_email ?? '', + logs.ip_address ?? '', + logs.user_agent ?? '', + ], + ) const headers = [ ' Event id', ' Created at', diff --git a/src/commands/fix/coana-fix.mts b/src/commands/fix/coana-fix.mts index 61ed607313..ba4531a8e8 100644 --- a/src/commands/fix/coana-fix.mts +++ b/src/commands/fix/coana-fix.mts @@ -158,6 +158,7 @@ export async function coanaFix( const sockSdk = sockSdkCResult.data const supportedFilesCResult = await fetchSupportedScanFileNames({ + orgSlug, spinner: silence ? undefined : spinner, silence, }) @@ -209,7 +210,9 @@ export async function coanaFix( } } const uploadCResult = await handleApiCall( - sockSdk.uploadManifestFiles(orgSlug, scanFilepaths, cwd), + sockSdk.uploadManifestFiles(orgSlug, scanFilepaths, { + pathsRelativeTo: cwd, + }), { description: 'upload manifests', spinner, diff --git a/src/commands/login/attempt-login.mts b/src/commands/login/attempt-login.mts index 44c2c435fa..79a6a015b2 100644 --- a/src/commands/login/attempt-login.mts +++ b/src/commands/login/attempt-login.mts @@ -71,8 +71,8 @@ export async function attemptLogin( const enterpriseOrgs = getEnterpriseOrgs(organizations) const enforcedChoices: OrgChoices = enterpriseOrgs.map(org => ({ - name: org.name ?? 'undefined', - value: org.id, + name: org['name'] ?? 'undefined', + value: org['id'], })) let enforcedOrgs: string[] = [] diff --git a/src/commands/organization/fetch-organization-list.mts b/src/commands/organization/fetch-organization-list.mts index 8ee53d5550..9da6c09d35 100644 --- a/src/commands/organization/fetch-organization-list.mts +++ b/src/commands/organization/fetch-organization-list.mts @@ -5,7 +5,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdk, SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { OrganizationsResult, SocketSdk } from '@socketsecurity/sdk' export type FetchOrganizationOptions = { description?: string | undefined @@ -20,8 +20,7 @@ export type EnterpriseOrganization = Omit & { export type EnterpriseOrganizations = EnterpriseOrganization[] -export type Organization = - SocketSdkSuccessResult<'getOrganizations'>['data']['organizations'][string] +export type Organization = OrganizationsResult['data']['organizations'][number] export type Organizations = Organization[] @@ -51,7 +50,7 @@ export async function fetchOrganization( sockSdk = sockSdkCResult.data } - const orgsCResult = await handleApiCall(sockSdk.getOrganizations(), { + const orgsCResult = await handleApiCall(sockSdk.listOrganizations(), { description, silence, }) diff --git a/src/commands/package/output-purls-shallow-score.mts b/src/commands/package/output-purls-shallow-score.mts index 92a00ffbde..c37b224122 100644 --- a/src/commands/package/output-purls-shallow-score.mts +++ b/src/commands/package/output-purls-shallow-score.mts @@ -241,20 +241,24 @@ export function preProcess( row.score.license = artifact.score?.license || 100 } - artifact.alerts?.forEach(({ severity, type }) => { - row.alerts.set(`${type}:${severity}`, { - type: (type as string) ?? 'unknown', - severity: (severity as string) ?? 'none', - }) - }) + artifact.alerts?.forEach( + ({ severity, type }: NonNullable[number]) => { + row.alerts.set(`${type}:${severity}`, { + type: (type as string) ?? 'unknown', + severity: (severity as string) ?? 'none', + }) + }, + ) } else { const alerts = new Map() - artifact.alerts?.forEach(({ severity, type }) => { - alerts.set(`${type}:${severity}`, { - type: (type as string) ?? 'unknown', - severity: (severity as string) ?? 'none', - }) - }) + artifact.alerts?.forEach( + ({ severity, type }: NonNullable[number]) => { + alerts.set(`${type}:${severity}`, { + type: (type as string) ?? 'unknown', + severity: (severity as string) ?? 'none', + }) + }, + ) rows.set(purl, { ecosystem: artifact.type, diff --git a/src/commands/repository/fetch-create-repo.mts b/src/commands/repository/fetch-create-repo.mts index af56fb9dcf..4066bc0085 100644 --- a/src/commands/repository/fetch-create-repo.mts +++ b/src/commands/repository/fetch-create-repo.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoryResult } from '@socketsecurity/sdk' export type FetchCreateRepoConfig = { defaultBranch: string @@ -21,7 +21,7 @@ export type FetchCreateRepoOptions = { export async function fetchCreateRepo( config: FetchCreateRepoConfig, options?: FetchCreateRepoOptions | undefined, -): Promise['data']>> { +): Promise> { const { defaultBranch, description, @@ -43,12 +43,11 @@ export async function fetchCreateRepo( const sockSdk = sockSdkCResult.data return await handleApiCall( - sockSdk.createOrgRepo(orgSlug, { + sockSdk.createRepository(orgSlug, repoName, { default_branch: defaultBranch, description, homepage, - name: repoName, - visibility, + visibility: visibility as 'private' | 'public', }), { description: 'to create a repository' }, ) diff --git a/src/commands/repository/fetch-delete-repo.mts b/src/commands/repository/fetch-delete-repo.mts index 4bde5cc292..223b17b64d 100644 --- a/src/commands/repository/fetch-delete-repo.mts +++ b/src/commands/repository/fetch-delete-repo.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { DeleteResult } from '@socketsecurity/sdk' export type FetchDeleteRepoOptions = { sdkOpts?: SetupSdkOptions | undefined @@ -13,7 +13,7 @@ export async function fetchDeleteRepo( orgSlug: string, repoName: string, options?: FetchDeleteRepoOptions | undefined, -): Promise['data']>> { +): Promise> { const { sdkOpts } = { __proto__: null, ...options, @@ -25,7 +25,7 @@ export async function fetchDeleteRepo( } const sockSdk = sockSdkCResult.data - return await handleApiCall(sockSdk.deleteOrgRepo(orgSlug, repoName), { + return await handleApiCall(sockSdk.deleteRepository(orgSlug, repoName), { description: 'to delete a repository', }) } diff --git a/src/commands/repository/fetch-list-all-repos.mts b/src/commands/repository/fetch-list-all-repos.mts index a7578ffcfe..6218642ccc 100644 --- a/src/commands/repository/fetch-list-all-repos.mts +++ b/src/commands/repository/fetch-list-all-repos.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoriesListResult } from '@socketsecurity/sdk' export type FetchListAllReposOptions = { direction?: string | undefined @@ -14,7 +14,7 @@ export type FetchListAllReposOptions = { export async function fetchListAllRepos( orgSlug: string, options?: FetchListAllReposOptions | undefined, -): Promise['data']>> { +): Promise> { const { direction, sdkOpts, sort } = { __proto__: null, ...options, @@ -26,7 +26,7 @@ export async function fetchListAllRepos( } const sockSdk = sockSdkCResult.data - const rows: SocketSdkSuccessResult<'getOrgRepoList'>['data']['results'] = [] + const rows: RepositoriesListResult['data']['results'] = [] let protection = 0 let nextPage = 0 while (nextPage >= 0) { @@ -39,11 +39,11 @@ export async function fetchListAllRepos( } // eslint-disable-next-line no-await-in-loop const orgRepoListCResult = await handleApiCall( - sockSdk.getOrgRepoList(orgSlug, { - sort, - direction, - per_page: String(100), // max - page: String(nextPage), + sockSdk.listRepositories(orgSlug, { + ...(sort ? { sort: sort as 'name' | 'updated_at' | 'created_at' } : {}), + ...(direction ? { direction: direction as 'asc' | 'desc' } : {}), + per_page: 100, // max + page: nextPage, }), { description: 'list of repositories' }, ) diff --git a/src/commands/repository/fetch-list-repos.mts b/src/commands/repository/fetch-list-repos.mts index 8e919be919..e332ef14b7 100644 --- a/src/commands/repository/fetch-list-repos.mts +++ b/src/commands/repository/fetch-list-repos.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoriesListResult } from '@socketsecurity/sdk' export type FetchListReposConfig = { direction: string @@ -20,7 +20,7 @@ export type FetchListReposOptions = { export async function fetchListRepos( config: FetchListReposConfig, options?: FetchListReposOptions | undefined, -): Promise['data']>> { +): Promise> { const { direction, orgSlug, page, perPage, sort } = { __proto__: null, ...config, @@ -38,11 +38,11 @@ export async function fetchListRepos( const sockSdk = sockSdkCResult.data return await handleApiCall( - sockSdk.getOrgRepoList(orgSlug, { - sort, - direction, - per_page: String(perPage), - page: String(page), + sockSdk.listRepositories(orgSlug, { + ...(sort ? { sort: sort as 'name' | 'updated_at' | 'created_at' } : {}), + ...(direction ? { direction: direction as 'asc' | 'desc' } : {}), + per_page: perPage, + page, }), { description: 'list of repositories' }, ) diff --git a/src/commands/repository/fetch-update-repo.mts b/src/commands/repository/fetch-update-repo.mts index 4dded9cebb..7f3cb6d90d 100644 --- a/src/commands/repository/fetch-update-repo.mts +++ b/src/commands/repository/fetch-update-repo.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoryResult } from '@socketsecurity/sdk' export type FetchUpdateRepoConfig = { defaultBranch: string @@ -21,7 +21,7 @@ export type FetchUpdateRepoOptions = { export async function fetchUpdateRepo( config: FetchUpdateRepoConfig, options?: FetchUpdateRepoOptions | undefined, -): Promise['data']>> { +): Promise> { const { defaultBranch, description, @@ -43,7 +43,7 @@ export async function fetchUpdateRepo( const sockSdk = sockSdkCResult.data return await handleApiCall( - sockSdk.updateOrgRepo(orgSlug, repoName, { + sockSdk.updateRepository(orgSlug, repoName, { default_branch: defaultBranch, description, homepage, diff --git a/src/commands/repository/fetch-view-repo.mts b/src/commands/repository/fetch-view-repo.mts index 4c4faf4bef..ee61d4734a 100644 --- a/src/commands/repository/fetch-view-repo.mts +++ b/src/commands/repository/fetch-view-repo.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoryResult } from '@socketsecurity/sdk' export type FetchViewRepoOptions = { sdkOpts?: SetupSdkOptions | undefined @@ -13,7 +13,7 @@ export async function fetchViewRepo( orgSlug: string, repoName: string, options?: FetchViewRepoOptions | undefined, -): Promise['data']>> { +): Promise> { const { sdkOpts } = { __proto__: null, ...options } as FetchViewRepoOptions const sockSdkCResult = await setupSdk(sdkOpts) @@ -22,7 +22,7 @@ export async function fetchViewRepo( } const sockSdk = sockSdkCResult.data - return await handleApiCall(sockSdk.getOrgRepo(orgSlug, repoName), { + return await handleApiCall(sockSdk.getRepository(orgSlug, repoName), { description: 'repository data', }) } diff --git a/src/commands/repository/handle-list-repos.mts b/src/commands/repository/handle-list-repos.mts index b1072e3c23..533865a848 100644 --- a/src/commands/repository/handle-list-repos.mts +++ b/src/commands/repository/handle-list-repos.mts @@ -38,12 +38,14 @@ export async function handleListRepos({ if (!data.ok) { await outputListRepos(data, outputKind, 0, 0, '', 0, direction) } else { - // Note: nextPage defaults to 0, is null when there's no next page + // Note: nextPage defaults to 0, is null when there's no next page. + // The SDK's strict type marks it optional (absent === no next page), so + // coalesce undefined to null to match outputListRepos's number | null. await outputListRepos( data, outputKind, page, - data.data.nextPage, + data.data.nextPage ?? null, sort, perPage, direction, diff --git a/src/commands/repository/output-create-repo.mts b/src/commands/repository/output-create-repo.mts index f90cf3cfc5..638320321a 100644 --- a/src/commands/repository/output-create-repo.mts +++ b/src/commands/repository/output-create-repo.mts @@ -4,10 +4,10 @@ import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoryResult } from '@socketsecurity/sdk' export function outputCreateRepo( - result: CResult['data']>, + result: CResult, requestedName: string, outputKind: OutputKind, ): void { diff --git a/src/commands/repository/output-delete-repo.mts b/src/commands/repository/output-delete-repo.mts index e515c33783..679bdbe634 100644 --- a/src/commands/repository/output-delete-repo.mts +++ b/src/commands/repository/output-delete-repo.mts @@ -4,10 +4,10 @@ import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { DeleteResult } from '@socketsecurity/sdk' export async function outputDeleteRepo( - result: CResult['data']>, + result: CResult, repoName: string, outputKind: OutputKind, ): Promise { diff --git a/src/commands/repository/output-list-repos.mts b/src/commands/repository/output-list-repos.mts index 62dac62678..8aa6428ba7 100644 --- a/src/commands/repository/output-list-repos.mts +++ b/src/commands/repository/output-list-repos.mts @@ -9,10 +9,10 @@ import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { Direction } from './types.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoriesListResult } from '@socketsecurity/sdk' export async function outputListRepos( - result: CResult['data']>, + result: CResult, outputKind: OutputKind, page: number, nextPage: number | null, diff --git a/src/commands/repository/output-update-repo.mts b/src/commands/repository/output-update-repo.mts index 6a0a5a01be..dfb47e54ae 100644 --- a/src/commands/repository/output-update-repo.mts +++ b/src/commands/repository/output-update-repo.mts @@ -4,10 +4,10 @@ import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoryResult } from '@socketsecurity/sdk' export async function outputUpdateRepo( - result: CResult['data']>, + result: CResult, repoName: string, outputKind: OutputKind, ): Promise { diff --git a/src/commands/repository/output-view-repo.mts b/src/commands/repository/output-view-repo.mts index 0f932cc556..66f21ad401 100644 --- a/src/commands/repository/output-view-repo.mts +++ b/src/commands/repository/output-view-repo.mts @@ -8,10 +8,10 @@ import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { RepositoryResult } from '@socketsecurity/sdk' export async function outputViewRepo( - result: CResult['data']>, + result: CResult, outputKind: OutputKind, ): Promise { if (!result.ok) { diff --git a/src/commands/scan/cmd-scan-create.test.mts b/src/commands/scan/cmd-scan-create.test.mts index 2693871f33..d1b1a888de 100644 --- a/src/commands/scan/cmd-scan-create.test.mts +++ b/src/commands/scan/cmd-scan-create.test.mts @@ -721,8 +721,11 @@ describe('socket scan create', async () => { async cmd => { const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) const output = stdout + stderr + // With SDK v4 the supported-manifest-files lookup is org-scoped, so an + // invalid token now fails at authentication before the glob runs; a valid + // token still reaches the "no eligible files" message on an empty dir. expect(output).toMatch( - /found no eligible files to scan|An error was thrown while requesting/, + /found no eligible files to scan|An error was thrown while requesting|Socket API error|Authentication failed/, ) expect( code, diff --git a/src/commands/scan/create-scan-from-github.mts b/src/commands/scan/create-scan-from-github.mts index 83bb3ec428..47537cf53c 100644 --- a/src/commands/scan/create-scan-from-github.mts +++ b/src/commands/scan/create-scan-from-github.mts @@ -57,7 +57,9 @@ export async function createScanFromGithub({ if (!result.ok) { return result } - targetRepos = result.data.results.map(obj => obj.slug || '') + targetRepos = result.data.results.map( + (obj: (typeof result.data.results)[number]) => obj.slug || '', + ) } targetRepos = targetRepos.map(s => s.trim()).filter(Boolean) @@ -213,6 +215,7 @@ async function scanOneRepo( repoSlug, defaultBranch, orgGithub, + orgSlug, repoApiUrl, githubToken, }) @@ -288,6 +291,7 @@ async function testAndDownloadManifestFiles({ files, githubToken, orgGithub, + orgSlug, repoApiUrl, repoSlug, tmpDir, @@ -297,6 +301,7 @@ async function testAndDownloadManifestFiles({ repoSlug: string defaultBranch: string orgGithub: string + orgSlug: string repoApiUrl: string githubToken: string }): Promise> { @@ -314,6 +319,7 @@ async function testAndDownloadManifestFiles({ file, tmpDir, defaultBranch, + orgSlug, repoApiUrl, githubToken, }) @@ -349,18 +355,20 @@ async function testAndDownloadManifestFile({ defaultBranch, file, githubToken, + orgSlug, repoApiUrl, tmpDir, }: { file: string tmpDir: string defaultBranch: string + orgSlug: string repoApiUrl: string githubToken: string }): Promise> { debugFn('notice', 'testing: file', file) - const supportedFilesCResult = await fetchSupportedScanFileNames() + const supportedFilesCResult = await fetchSupportedScanFileNames({ orgSlug }) const supportedFiles = supportedFilesCResult.ok ? supportedFilesCResult.data : undefined diff --git a/src/commands/scan/fetch-create-org-full-scan.mts b/src/commands/scan/fetch-create-org-full-scan.mts index 34e5aadf9e..b16ee7f789 100644 --- a/src/commands/scan/fetch-create-org-full-scan.mts +++ b/src/commands/scan/fetch-create-org-full-scan.mts @@ -9,7 +9,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { FullScanResult } from '@socketsecurity/sdk' export type FetchCreateOrgFullScanConfigs = { branchName: string @@ -35,7 +35,7 @@ export async function fetchCreateOrgFullScan( orgSlug: string, config: FetchCreateOrgFullScanConfigs, options?: FetchCreateOrgFullScanOptions | undefined, -): Promise['data']>> { +): Promise> { const { branchName, commitHash, @@ -75,18 +75,23 @@ export async function fetchCreateOrgFullScan( } return await handleApiCall( - sockSdk.createOrgFullScan(orgSlug, packagePaths, cwd, { + sockSdk.createFullScan(orgSlug, packagePaths, { + pathsRelativeTo: cwd, ...(branchName ? { branch: branchName } : {}), ...(commitHash ? { commit_hash: commitHash } : {}), ...(commitMessage ? { commit_message: commitMessage } : {}), ...(committers ? { committers } : {}), - make_default_branch: String(defaultBranch), - ...(pullRequest ? { pull_request: String(pullRequest) } : {}), - scan_type: scanType, + ...(defaultBranch !== undefined + ? { make_default_branch: defaultBranch } + : {}), + ...(pullRequest ? { pull_request: pullRequest } : {}), + ...(scanType ? { scan_type: scanType } : {}), repo: repoName, ...(workspace ? { workspace } : {}), - set_as_pending_head: String(pendingHead), - tmp: String(tmp), + ...(pendingHead !== undefined + ? { set_as_pending_head: pendingHead } + : {}), + ...(tmp !== undefined ? { tmp } : {}), }), { description: 'to create a scan' }, ) diff --git a/src/commands/scan/fetch-delete-org-full-scan.mts b/src/commands/scan/fetch-delete-org-full-scan.mts index 82ac1638c9..74bffb4639 100644 --- a/src/commands/scan/fetch-delete-org-full-scan.mts +++ b/src/commands/scan/fetch-delete-org-full-scan.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { DeleteResult } from '@socketsecurity/sdk' export type FetchDeleteOrgFullScanOptions = { sdkOpts?: SetupSdkOptions | undefined @@ -13,7 +13,7 @@ export async function fetchDeleteOrgFullScan( orgSlug: string, scanId: string, options?: FetchDeleteOrgFullScanOptions | undefined, -): Promise['data']>> { +): Promise> { const { sdkOpts } = { __proto__: null, ...options, @@ -25,7 +25,7 @@ export async function fetchDeleteOrgFullScan( } const sockSdk = sockSdkCResult.data - return await handleApiCall(sockSdk.deleteOrgFullScan(orgSlug, scanId), { + return await handleApiCall(sockSdk.deleteFullScan(orgSlug, scanId), { description: 'to delete a scan', }) } diff --git a/src/commands/scan/fetch-list-scans.mts b/src/commands/scan/fetch-list-scans.mts index b7de0c83ca..442825aeab 100644 --- a/src/commands/scan/fetch-list-scans.mts +++ b/src/commands/scan/fetch-list-scans.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { FullScanListResult } from '@socketsecurity/sdk' export type FetchOrgFullScanListConfig = { branch: string @@ -23,7 +23,7 @@ export type FetchOrgFullScanListOptions = { export async function fetchOrgFullScanList( config: FetchOrgFullScanListConfig, options?: FetchOrgFullScanListOptions | undefined, -): Promise['data']>> { +): Promise> { const { sdkOpts } = { __proto__: null, ...options, @@ -41,14 +41,14 @@ export async function fetchOrgFullScanList( } as FetchOrgFullScanListConfig return await handleApiCall( - sockSdk.getOrgFullScanList(orgSlug, { + sockSdk.listFullScans(orgSlug, { ...(branch ? { branch } : {}), ...(repo ? { repo } : {}), - sort, - direction, + ...(sort ? { sort: sort as 'name' | 'created_at' } : {}), + ...(direction ? { direction: direction as 'asc' | 'desc' } : {}), from: from_time, - page: String(page), - per_page: String(perPage), + page, + per_page: perPage, }), { description: 'list of scans' }, ) diff --git a/src/commands/scan/fetch-scan-metadata.mts b/src/commands/scan/fetch-scan-metadata.mts index d7b3b46343..e49fedeaae 100644 --- a/src/commands/scan/fetch-scan-metadata.mts +++ b/src/commands/scan/fetch-scan-metadata.mts @@ -3,7 +3,7 @@ import { setupSdk } from '../../utils/sdk.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { FullScanResult } from '@socketsecurity/sdk' export type FetchScanMetadataOptions = { sdkOpts?: SetupSdkOptions | undefined @@ -13,7 +13,7 @@ export async function fetchScanMetadata( orgSlug: string, scanId: string, options?: FetchScanMetadataOptions | undefined, -): Promise['data']>> { +): Promise> { const { sdkOpts } = { __proto__: null, ...options, @@ -25,7 +25,7 @@ export async function fetchScanMetadata( } const sockSdk = sockSdkCResult.data - return await handleApiCall(sockSdk.getOrgFullScanMetadata(orgSlug, scanId), { + return await handleApiCall(sockSdk.getFullScanMetadata(orgSlug, scanId), { description: 'meta data for a full scan', }) } diff --git a/src/commands/scan/fetch-scan.mts b/src/commands/scan/fetch-scan.mts index 35389c28f2..8df41bfd63 100644 --- a/src/commands/scan/fetch-scan.mts +++ b/src/commands/scan/fetch-scan.mts @@ -1,47 +1,78 @@ import { debugDir, debugFn } from '@socketsecurity/registry/lib/debug' -import { queryApiSafeText } from '../../utils/api.mts' +import { queryApiSafeTextWithStatus } from '../../utils/api.mts' import type { CResult } from '../../types.mts' import type { SocketArtifact } from '../../utils/alert/artifact.mts' +// HTTP 202 Accepted: cached results are still being computed; poll again. +const HTTP_STATUS_ACCEPTED = 202 + +export const CACHED_POLL_INITIAL_DELAY_MS = 1000 +export const CACHED_POLL_MAX_DELAY_MS = 10_000 +export const CACHED_POLL_TIMEOUT_MS = 10 * 60 * 1000 + +function sleep(ms: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, ms) + }) +} + export async function fetchScan( orgSlug: string, scanId: string, ): Promise> { - const result = await queryApiSafeText( - `orgs/${orgSlug}/full-scans/${encodeURIComponent(scanId)}`, - 'a scan', - ) - - if (!result.ok) { - return result + // Serve pre-computed results from the immutable store (`?cached=true`): a + // 200 carries the ndjson body, a 202 means the server enqueued a background + // job to compute them — poll with backoff until the results are ready, so + // callers only ever observe the final scan. + const path = `orgs/${orgSlug}/full-scans/${encodeURIComponent(scanId)}?cached=true` + const deadline = Date.now() + CACHED_POLL_TIMEOUT_MS + let delayMs = CACHED_POLL_INITIAL_DELAY_MS + for (;;) { + // eslint-disable-next-line no-await-in-loop + const result = await queryApiSafeTextWithStatus(path, 'a scan') + if (!result.ok) { + return result + } + if (result.data.status !== HTTP_STATUS_ACCEPTED) { + return parseArtifactsNdjson(result.data.text) + } + if (Date.now() >= deadline) { + return { + ok: false, + message: 'Scan results not ready', + cause: `The Socket API is still computing cached results for scan ${scanId} after ${CACHED_POLL_TIMEOUT_MS / 60_000} minutes (path: ${path}). Retry in a few minutes — the server keeps computing in the background.`, + } + } + // eslint-disable-next-line no-await-in-loop + await sleep(delayMs) + delayMs = Math.min(delayMs * 2, CACHED_POLL_MAX_DELAY_MS) } +} - const jsonsString = result.data - - // This is nd-json; each line is a json object +export function parseArtifactsNdjson( + jsonsString: string, +): CResult { + // This is nd-json; each line is a json object. const lines = jsonsString.split('\n').filter(Boolean) - let ok = true - const data = lines.map(line => { + const data: SocketArtifact[] = [] + + for (let i = 0, { length } = lines; i < length; i += 1) { + const line = lines[i]! try { - return JSON.parse(line) + data.push(JSON.parse(line)) } catch (e) { - ok = false debugFn('error', 'Failed to parse scan result line as JSON') debugDir('error', { error: e, line }) - return undefined + return { + ok: false, + message: 'Invalid Socket API response', + cause: + 'The Socket API responded with at least one line that was not valid JSON. Please report if this persists.', + } } - }) as unknown as SocketArtifact[] - - if (ok) { - return { ok: true, data } } - return { - ok: false, - message: 'Invalid Socket API response', - cause: - 'The Socket API responded with at least one line that was not valid JSON. Please report if this persists.', - } + return { ok: true, data } } diff --git a/src/commands/scan/fetch-scan.test.mts b/src/commands/scan/fetch-scan.test.mts new file mode 100644 index 0000000000..2bb9e80e7e --- /dev/null +++ b/src/commands/scan/fetch-scan.test.mts @@ -0,0 +1,93 @@ +process.env['SOCKET_CLI_API_TOKEN'] = 'test-token' +process.env['SOCKET_CLI_API_BASE_URL'] = 'https://api.socket.dev/v0/' + +import nock from 'nock' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' + +import { fetchScan, parseArtifactsNdjson } from './fetch-scan.mts' + +// Drives the real direct-API path through nock (an external HTTP double) rather +// than stubbing owned modules. Cached scan reads hit ?cached=true and poll on +// 202 until a 200 arrives. +const BASE_HOST = 'https://api.socket.dev' + +const NDJSON = + '{"type":"npm","name":"lodash","version":"4.17.21"}\n' + + '{"type":"npm","name":"react","version":"18.2.0"}\n' + +describe('fetchScan', () => { + beforeEach(() => { + nock.cleanAll() + nock.disableNetConnect() + }) + + afterEach(() => { + nock.cleanAll() + nock.enableNetConnect() + }) + + it('returns cached artifacts on a 200 cache hit', async () => { + nock(BASE_HOST) + .get('/v0/orgs/test-org/full-scans/scan-1') + .query({ cached: 'true' }) + .reply(200, NDJSON) + + const result = await fetchScan('test-org', 'scan-1') + + expect(result.ok).toBe(true) + expect((result as { data: unknown[] }).data).toEqual([ + { type: 'npm', name: 'lodash', version: '4.17.21' }, + { type: 'npm', name: 'react', version: '18.2.0' }, + ]) + }) + + it('polls on 202 until the cached result is ready', async () => { + nock(BASE_HOST) + .get('/v0/orgs/test-org/full-scans/scan-2') + .query({ cached: 'true' }) + .reply(202, { status: 'processing', id: 'scan-2' }) + nock(BASE_HOST) + .get('/v0/orgs/test-org/full-scans/scan-2') + .query({ cached: 'true' }) + .reply(200, NDJSON) + + const result = await fetchScan('test-org', 'scan-2') + + expect(result.ok).toBe(true) + expect((result as { data: unknown[] }).data).toHaveLength(2) + }) + + it('maps a 404 to a failed CResult', async () => { + nock(BASE_HOST) + .get('/v0/orgs/test-org/full-scans/missing') + .query({ cached: 'true' }) + .reply(404, { error: { message: 'Not found' } }) + + const result = await fetchScan('test-org', 'missing') + + expect(result.ok).toBe(false) + expect(result).toMatchObject({ + message: 'Socket API error', + data: { code: 404 }, + }) + }) +}) + +describe('parseArtifactsNdjson', () => { + it('parses one artifact per line, skipping blanks', () => { + const result = parseArtifactsNdjson(NDJSON) + expect(result).toEqual({ + ok: true, + data: [ + { type: 'npm', name: 'lodash', version: '4.17.21' }, + { type: 'npm', name: 'react', version: '18.2.0' }, + ], + }) + }) + + it('returns an error when a line is not valid JSON', () => { + const result = parseArtifactsNdjson('{"ok":true}\nnot-json\n') + expect(result.ok).toBe(false) + expect(result).toMatchObject({ message: 'Invalid Socket API response' }) + }) +}) diff --git a/src/commands/scan/fetch-supported-scan-file-names.mts b/src/commands/scan/fetch-supported-scan-file-names.mts index 4f9472363b..4fcfc2fd75 100644 --- a/src/commands/scan/fetch-supported-scan-file-names.mts +++ b/src/commands/scan/fetch-supported-scan-file-names.mts @@ -1,5 +1,6 @@ import { handleApiCall } from '../../utils/api.mts' import { setupSdk } from '../../utils/sdk.mts' +import { getDefaultOrgSlug } from '../ci/fetch-default-org-slug.mts' import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' @@ -7,6 +8,7 @@ import type { Spinner } from '@socketsecurity/registry/lib/spinner' import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' export type FetchSupportedScanFileNamesOptions = { + orgSlug?: string | undefined sdkOpts?: SetupSdkOptions | undefined spinner?: Spinner | undefined silence?: boolean | undefined @@ -14,8 +16,9 @@ export type FetchSupportedScanFileNamesOptions = { export async function fetchSupportedScanFileNames( options?: FetchSupportedScanFileNamesOptions | undefined, -): Promise['data']>> { +): Promise['data']>> { const { + orgSlug, sdkOpts, silence = false, spinner, @@ -30,7 +33,18 @@ export async function fetchSupportedScanFileNames( } const sockSdk = sockSdkCResult.data - return await handleApiCall(sockSdk.getSupportedScanFiles(), { + // getSupportedFiles is org-scoped in SDK v4. Use the provided org or + // discover the default one. + let resolvedOrgSlug = orgSlug + if (!resolvedOrgSlug) { + const orgSlugCResult = await getDefaultOrgSlug() + if (!orgSlugCResult.ok) { + return orgSlugCResult + } + resolvedOrgSlug = orgSlugCResult.data + } + + return await handleApiCall(sockSdk.getSupportedFiles(resolvedOrgSlug), { description: 'supported scan file types', spinner, silence, diff --git a/src/commands/scan/generate-report.mts b/src/commands/scan/generate-report.mts index e071df2b6b..ecbc42fd05 100644 --- a/src/commands/scan/generate-report.mts +++ b/src/commands/scan/generate-report.mts @@ -247,7 +247,10 @@ function createLeaf( type: alert.type, policy: policyAction, url: getSocketDevPackageOverviewUrlFromPurl(art), - manifest: art.manifestFiles?.map(o => o.file) ?? [], + manifest: + art.manifestFiles?.map( + (o: NonNullable[number]) => o.file, + ) ?? [], } return leaf } diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index 9e18e879cc..7cb50813c7 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -38,13 +38,15 @@ import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' const PREGENERATED_SBOM_KEYS = ['cdx', 'socket', 'spdx'] function getPregeneratedSbomPatterns( - supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'], + supportedFiles: SocketSdkSuccessResult<'getSupportedFiles'>['data'], ): string[] { const patterns: string[] = [] for (const key of PREGENERATED_SBOM_KEYS) { const supported = supportedFiles[key] if (supported) { - for (const entry of Object.values(supported)) { + for (const entry of Object.values( + supported as Record, + )) { patterns.push(`**/${entry.pattern}`) } } @@ -54,7 +56,7 @@ function getPregeneratedSbomPatterns( function filterToPregeneratedSboms( filepaths: string[], - supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'], + supportedFiles: SocketSdkSuccessResult<'getSupportedFiles'>['data'], ): string[] { const patterns = getPregeneratedSbomPatterns(supportedFiles) // `dot: true` lets `*`-prefixed patterns match leading-dot filenames such as @@ -162,7 +164,10 @@ export async function handleCreateNewScan({ const { spinner } = constants - const supportedFilesCResult = await fetchSupportedScanFileNames({ spinner }) + const supportedFilesCResult = await fetchSupportedScanFileNames({ + orgSlug, + spinner, + }) if (!supportedFilesCResult.ok) { debugFn('warn', 'Failed to fetch supported scan file names') debugDir('inspect', { supportedFilesCResult }) diff --git a/src/commands/scan/handle-scan-reach.mts b/src/commands/scan/handle-scan-reach.mts index 60c4d07d1e..b1bf3399ac 100644 --- a/src/commands/scan/handle-scan-reach.mts +++ b/src/commands/scan/handle-scan-reach.mts @@ -36,7 +36,10 @@ export async function handleScanReach({ const { spinner } = constants // Get supported file names. - const supportedFilesCResult = await fetchSupportedScanFileNames({ spinner }) + const supportedFilesCResult = await fetchSupportedScanFileNames({ + orgSlug, + spinner, + }) if (!supportedFilesCResult.ok) { await outputScanReach(supportedFilesCResult, { cwd, diff --git a/src/commands/scan/output-create-new-scan.mts b/src/commands/scan/output-create-new-scan.mts index f927bfd368..fb25a33641 100644 --- a/src/commands/scan/output-create-new-scan.mts +++ b/src/commands/scan/output-create-new-scan.mts @@ -10,7 +10,7 @@ import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' import type { Spinner } from '@socketsecurity/registry/lib/spinner' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { FullScanResult } from '@socketsecurity/sdk' export type CreateNewScanOptions = { interactive?: boolean | undefined @@ -19,7 +19,7 @@ export type CreateNewScanOptions = { } export async function outputCreateNewScan( - result: CResult['data']>, + result: CResult, options?: CreateNewScanOptions | undefined, ) { const { diff --git a/src/commands/scan/output-delete-scan.mts b/src/commands/scan/output-delete-scan.mts index 3ee8965f15..0c4b00fdf0 100644 --- a/src/commands/scan/output-delete-scan.mts +++ b/src/commands/scan/output-delete-scan.mts @@ -4,10 +4,10 @@ import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { DeleteResult } from '@socketsecurity/sdk' export async function outputDeleteScan( - result: CResult['data']>, + result: CResult, outputKind: OutputKind, ): Promise { if (!result.ok) { diff --git a/src/commands/scan/output-diff-scan.mts b/src/commands/scan/output-diff-scan.mts index 0232591e93..72cd494491 100644 --- a/src/commands/scan/output-diff-scan.mts +++ b/src/commands/scan/output-diff-scan.mts @@ -123,9 +123,11 @@ async function handleMarkdown( logger.log(`- Added packages: ${data.artifacts.added.length}`) if (data.artifacts.added.length > 0) { - data.artifacts.added.slice(0, 10).forEach(artifact => { - logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) - }) + data.artifacts.added + .slice(0, 10) + .forEach((artifact: (typeof data.artifacts.added)[number]) => { + logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) + }) if (data.artifacts.added.length > 10) { logger.log(` ... and ${data.artifacts.added.length - 10} more`) } @@ -133,9 +135,11 @@ async function handleMarkdown( logger.log(`- Removed packages: ${data.artifacts.removed.length}`) if (data.artifacts.removed.length > 0) { - data.artifacts.removed.slice(0, 10).forEach(artifact => { - logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) - }) + data.artifacts.removed + .slice(0, 10) + .forEach((artifact: (typeof data.artifacts.removed)[number]) => { + logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) + }) if (data.artifacts.removed.length > 10) { logger.log(` ... and ${data.artifacts.removed.length - 10} more`) } @@ -143,9 +147,11 @@ async function handleMarkdown( logger.log(`- Replaced packages: ${data.artifacts.replaced.length}`) if (data.artifacts.replaced.length > 0) { - data.artifacts.replaced.slice(0, 10).forEach(artifact => { - logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) - }) + data.artifacts.replaced + .slice(0, 10) + .forEach((artifact: (typeof data.artifacts.replaced)[number]) => { + logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) + }) if (data.artifacts.replaced.length > 10) { logger.log(` ... and ${data.artifacts.replaced.length - 10} more`) } @@ -153,9 +159,11 @@ async function handleMarkdown( logger.log(`- Updated packages: ${data.artifacts.updated.length}`) if (data.artifacts.updated.length > 0) { - data.artifacts.updated.slice(0, 10).forEach(artifact => { - logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) - }) + data.artifacts.updated + .slice(0, 10) + .forEach((artifact: (typeof data.artifacts.updated)[number]) => { + logger.log(` - ${artifact.type} ${artifact.name}@${artifact.version}`) + }) if (data.artifacts.updated.length > 10) { logger.log(` ... and ${data.artifacts.updated.length - 10} more`) } diff --git a/src/commands/scan/output-list-scans.mts b/src/commands/scan/output-list-scans.mts index 30132fd02c..4e677f6392 100644 --- a/src/commands/scan/output-list-scans.mts +++ b/src/commands/scan/output-list-scans.mts @@ -8,10 +8,10 @@ import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { FullScanListResult } from '@socketsecurity/sdk' export async function outputListScans( - result: CResult['data']>, + result: CResult, outputKind: OutputKind, ): Promise { if (!result.ok) { @@ -37,21 +37,23 @@ export async function outputListScans( ], } - const formattedResults = result.data.results.map(d => { - return { - id: d.id, - report_url: colors.underline(`${d.html_report_url}`), - created_at: d.created_at - ? new Date(d.created_at).toLocaleDateString('en-us', { - year: 'numeric', - month: 'numeric', - day: 'numeric', - }) - : '', - repo: d.repo, - branch: d.branch, - } - }) + const formattedResults = result.data.results.map( + (d: FullScanListResult['data']['results'][number]) => { + return { + id: d.id, + report_url: colors.underline(`${d.html_report_url}`), + created_at: d.created_at + ? new Date(d.created_at).toLocaleDateString('en-us', { + year: 'numeric', + month: 'numeric', + day: 'numeric', + }) + : '', + repo: d.repo, + branch: d.branch, + } + }, + ) logger.log(chalkTable(options, formattedResults)) } diff --git a/src/commands/scan/output-scan-metadata.mts b/src/commands/scan/output-scan-metadata.mts index f20f642968..47bcc8afb8 100644 --- a/src/commands/scan/output-scan-metadata.mts +++ b/src/commands/scan/output-scan-metadata.mts @@ -4,10 +4,10 @@ import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts' import { serializeResultJson } from '../../utils/serialize-result-json.mts' import type { CResult, OutputKind } from '../../types.mts' -import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' +import type { FullScanResult } from '@socketsecurity/sdk' export async function outputScanMetadata( - result: CResult['data']>, + result: CResult, scanId: string, outputKind: OutputKind, ): Promise { diff --git a/src/commands/scan/perform-reachability-analysis.mts b/src/commands/scan/perform-reachability-analysis.mts index 37fb7d4f98..69e7973a9c 100644 --- a/src/commands/scan/perform-reachability-analysis.mts +++ b/src/commands/scan/perform-reachability-analysis.mts @@ -144,11 +144,9 @@ export async function performReachabilityAnalysis( // facts files are cleaned up downstream — see the post-success // deletion in handle-create-new-scan.mts. const uploadCResult = await handleApiCall( - sockSdk.uploadManifestFiles( - orgSlug, - packagePaths, - path.resolve(cwd, analysisTarget), - ), + sockSdk.uploadManifestFiles(orgSlug, packagePaths, { + pathsRelativeTo: path.resolve(cwd, analysisTarget), + }), { description: 'upload manifests', spinner, diff --git a/src/commands/scan/stream-scan.mts b/src/commands/scan/stream-scan.mts index fc7420196c..6ff805f472 100644 --- a/src/commands/scan/stream-scan.mts +++ b/src/commands/scan/stream-scan.mts @@ -3,7 +3,9 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { handleApiCall } from '../../utils/api.mts' import { setupSdk } from '../../utils/sdk.mts' +import type { CResult } from '../../types.mts' import type { SetupSdkOptions } from '../../utils/sdk.mts' +import type { SocketSdkSuccessResult } from '@socketsecurity/sdk' export type StreamScanOptions = { file?: string | undefined @@ -14,7 +16,7 @@ export async function streamScan( orgSlug: string, scanId: string, options?: StreamScanOptions | undefined, -) { +): Promise['data']>> { const { file, sdkOpts } = { __proto__: null, ...options, @@ -29,7 +31,9 @@ export async function streamScan( // Note: this will write to stdout or target file. It's not a noop return await handleApiCall( - sockSdk.getOrgFullScan(orgSlug, scanId, file === '-' ? undefined : file), + sockSdk.streamFullScan(orgSlug, scanId, { + output: file === '-' ? undefined : file, + }), { description: 'a scan' }, ) } diff --git a/src/utils/api.mts b/src/utils/api.mts index 564eed931d..80d308df86 100644 --- a/src/utils/api.mts +++ b/src/utils/api.mts @@ -44,12 +44,6 @@ import { getCliUserAgent, getDefaultApiToken, getExtraCaCerts } from './sdk.mts' import type { CResult } from '../types.mts' import type { Spinner } from '@socketsecurity/registry/lib/spinner' -import type { - SocketSdkErrorResult, - SocketSdkOperations, - SocketSdkResult, - SocketSdkSuccessResult, -} from '@socketsecurity/sdk' const MAX_REDIRECTS = 20 const NO_ERROR_MESSAGE = 'No error message returned' @@ -288,17 +282,58 @@ export type HandleApiCallOptions = { commandPath?: string | undefined } -export type ApiCallResult = CResult< - SocketSdkSuccessResult['data'] +/** + * The subset of a Socket SDK result that {@link handleApiCall} reads. Both the + * OpenAPI-derived `SocketSdkResult` family and the SDK 4.x strict result + * types (`OrganizationsResult`, `RepositoryResult`, `DeleteResult`, ...) + * structurally satisfy this shape, so a caller can pass whichever result a + * given SDK method returns without forcing a (sometimes stale) operation-key + * shape onto the strict return types. + */ +export type SocketSdkReturnedResult = + | { + cause?: undefined + data: unknown + error?: undefined + status: number + success: true + } + | { + cause?: string | undefined + data?: undefined + error: string + status: number + success: false + url?: string | undefined + } + +/** + * The success `data` type carried by a Socket SDK result union `R`. The + * conditional distributes over `R`'s members, so only the success member(s) + * contribute their `data`; the error member resolves to `never` and drops out. + * Inferring the whole result `R` and extracting here (rather than inferring a + * bare `Data` from a `Promise<{ data: Data } | ...>`) avoids `Data` absorbing + * the error branch's `data?: undefined` — which would otherwise widen every + * result to `CResult`. + */ +export type ApiCallSuccessData = R extends { + success: true + data: infer Data +} + ? Data + : never + +export type ApiCallResult = CResult< + ApiCallSuccessData > /** * Handle Socket SDK API calls with error handling and permission logging. */ -export async function handleApiCall( - value: Promise>, +export async function handleApiCall( + value: Promise, options?: HandleApiCallOptions | undefined, -): Promise> { +): Promise> { const { commandPath, description, @@ -317,7 +352,7 @@ export async function handleApiCall( } } - let sdkResult: SocketSdkResult + let sdkResult: R try { sdkResult = await value if (!silence) { @@ -336,7 +371,7 @@ export async function handleApiCall( } } catch (e) { spinner?.stop() - const socketSdkErrorResult: ApiCallResult = { + const socketSdkErrorResult: ApiCallResult = { ok: false, message: 'Socket API error', cause: messageWithCauses(e as Error), @@ -349,22 +384,22 @@ export async function handleApiCall( return socketSdkErrorResult } - // Note: TS can't narrow down the type of result due to generics. + // The `success` discriminant is a concrete boolean literal in both branches, + // so this narrows the result to its error branch regardless of `R`. if (sdkResult.success === false) { const endpoint = description || 'Socket API' - debugApiResponse('API', endpoint, sdkResult.status as number) + debugApiResponse('API', endpoint, sdkResult.status) debugDir('inspect', { sdkResult }) - const errCResult = sdkResult as SocketSdkErrorResult - const errStr = errCResult.error ? String(errCResult.error).trim() : '' + const errStr = sdkResult.error ? String(sdkResult.error).trim() : '' const message = errStr || NO_ERROR_MESSAGE - const reason = errCResult.cause || NO_ERROR_MESSAGE + const reason = sdkResult.cause || NO_ERROR_MESSAGE const baseCause = reason && message !== reason ? `${message} (reason: ${reason})` : message - const cause = errCResult.url - ? `${baseCause} (url: ${errCResult.url})` + const cause = sdkResult.url + ? `${baseCause} (url: ${sdkResult.url})` : baseCause - const socketSdkErrorResult: ApiCallResult = { + const socketSdkErrorResult: ApiCallResult = { ok: false, message: 'Socket API error', cause, @@ -380,18 +415,21 @@ export async function handleApiCall( return socketSdkErrorResult } - const socketSdkSuccessResult: ApiCallResult = { + // Narrowing a type parameter by its `success` discriminant resolves `.data` + // through the constraint's success branch (`unknown`); re-apply the precise + // per-call success type the return contract already guarantees. + const socketSdkSuccessResult: ApiCallResult = { ok: true, - data: (sdkResult as SocketSdkSuccessResult).data, + data: sdkResult.data as ApiCallSuccessData, } return socketSdkSuccessResult } -export async function handleApiCallNoSpinner( - value: Promise>, +export async function handleApiCallNoSpinner( + value: Promise, description: string, -): Promise['data']>> { - let sdkResult: SocketSdkResult +): Promise> { + let sdkResult: R try { sdkResult = await value } catch (e) { @@ -410,21 +448,19 @@ export async function handleApiCallNoSpinner( } } - // Note: TS can't narrow down the type of result due to generics + // The `success` discriminant is a concrete boolean literal in both branches, + // so this narrows the result to its error branch regardless of `R`. if (sdkResult.success === false) { debugFn('error', `fail: ${description} bad response`) debugDir('inspect', { sdkResult }) - const sdkErrorResult = sdkResult as SocketSdkErrorResult - const errStr = sdkErrorResult.error - ? String(sdkErrorResult.error).trim() - : '' + const errStr = sdkResult.error ? String(sdkResult.error).trim() : '' const message = errStr || NO_ERROR_MESSAGE - const reason = sdkErrorResult.cause || NO_ERROR_MESSAGE + const reason = sdkResult.cause || NO_ERROR_MESSAGE const baseCause = reason && message !== reason ? `${message} (reason: ${reason})` : message - const cause = sdkErrorResult.url - ? `${baseCause} (url: ${sdkErrorResult.url})` + const cause = sdkResult.url + ? `${baseCause} (url: ${sdkResult.url})` : baseCause return { @@ -436,10 +472,12 @@ export async function handleApiCallNoSpinner( }, } } else { - const sdkSuccessResult = sdkResult as SocketSdkSuccessResult + // Narrowing a type parameter by its `success` discriminant resolves `.data` + // through the constraint's success branch (`unknown`); re-apply the precise + // per-call success type the return contract already guarantees. return { ok: true, - data: sdkSuccessResult.data, + data: sdkResult.data as ApiCallSuccessData, } } } @@ -461,14 +499,22 @@ async function queryApi(path: string, apiToken: string) { return result } +export type ApiTextResult = { + status: number + text: string +} + /** - * Query Socket API endpoint and return text response with error handling. + * Query a Socket API endpoint and return the HTTP status alongside the text + * body, with error handling. Unlike queryApiSafeText this surfaces the status + * on success (including 2xx statuses like 202 Accepted), so callers can drive + * status-dependent flows such as the cached-scan 202 poll loop. */ -export async function queryApiSafeText( +export async function queryApiSafeTextWithStatus( path: string, description?: string | undefined, commandPath?: string | undefined, -): Promise> { +): Promise> { const apiToken = getDefaultApiToken() if (!apiToken) { return { @@ -546,10 +592,13 @@ export async function queryApiSafeText( } try { - const data = await result.text() + const text = await result.text() return { ok: true, - data, + data: { + status: result.status, + text, + }, } } catch (e) { debugFn('error', 'Failed to read API response text') @@ -563,6 +612,22 @@ export async function queryApiSafeText( } } +/** + * Query Socket API endpoint and return text response with error handling. + */ +export async function queryApiSafeText( + path: string, + description?: string | undefined, + commandPath?: string | undefined, +): Promise> { + const result = await queryApiSafeTextWithStatus( + path, + description, + commandPath, + ) + return result.ok ? { ok: true, data: result.data.text } : result +} + /** * Query Socket API endpoint and return parsed JSON response. */ diff --git a/src/utils/ecosystem.mts b/src/utils/ecosystem.mts index c514c21961..597457b577 100644 --- a/src/utils/ecosystem.mts +++ b/src/utils/ecosystem.mts @@ -50,6 +50,7 @@ export const ALL_ECOSYSTEMS = [ 'bitbucket', 'cargo', 'chrome', + 'clawhub', 'cocoapods', 'composer', 'conan', @@ -73,6 +74,7 @@ export const ALL_ECOSYSTEMS = [ 'pypi', 'qpkg', 'rpm', + 'socket', 'swift', 'swid', 'unknown', diff --git a/src/utils/glob.mts b/src/utils/glob.mts index 21c3838fd3..b5e2c35d2d 100644 --- a/src/utils/glob.mts +++ b/src/utils/glob.mts @@ -182,7 +182,7 @@ function workspacePatternToGlobPattern(workspace: string): string { export function filterBySupportedScanFiles( filepaths: string[] | readonly string[], - supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'], + supportedFiles: SocketSdkSuccessResult<'getSupportedFiles'>['data'], ): string[] { const patterns = getSupportedFilePatterns(supportedFiles) return filepaths.filter(p => @@ -191,7 +191,7 @@ export function filterBySupportedScanFiles( } export function createSupportedFilesFilter( - supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'], + supportedFiles: SocketSdkSuccessResult<'getSupportedFiles'>['data'], ): (filepath: string) => boolean { const patterns = getSupportedFilePatterns(supportedFiles) return (filepath: string) => @@ -199,13 +199,17 @@ export function createSupportedFilesFilter( } export function getSupportedFilePatterns( - supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'], + supportedFiles: SocketSdkSuccessResult<'getSupportedFiles'>['data'], ): string[] { const patterns: string[] = [] for (const key of Object.keys(supportedFiles)) { const supported = supportedFiles[key] if (supported) { - patterns.push(...Object.values(supported).map(p => `**/${p.pattern}`)) + patterns.push( + ...Object.values(supported as Record).map( + p => `**/${p.pattern}`, + ), + ) } } return patterns @@ -391,7 +395,7 @@ export async function globWorkspace( export function isReportSupportedFile( filepath: string, - supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'], + supportedFiles: SocketSdkSuccessResult<'getSupportedFiles'>['data'], ) { const patterns = getSupportedFilePatterns(supportedFiles) return micromatch.some(filepath, patterns, { dot: true, nocase: true }) diff --git a/src/utils/path-resolve.mts b/src/utils/path-resolve.mts index 247d81ede0..12dd96ad3f 100644 --- a/src/utils/path-resolve.mts +++ b/src/utils/path-resolve.mts @@ -132,7 +132,7 @@ function normalizeScanInputPath(pathToNormalize: string, cwd: string): string { export async function getPackageFilesForScan( inputPaths: string[], - supportedFiles: SocketSdkSuccessResult<'getReportSupportedFiles'>['data'], + supportedFiles: SocketSdkSuccessResult<'getSupportedFiles'>['data'], options?: PackageFilesForScanOptions | undefined, ): Promise { const { diff --git a/src/utils/sdk.mts b/src/utils/sdk.mts index 00ec18097c..06614697c3 100644 --- a/src/utils/sdk.mts +++ b/src/utils/sdk.mts @@ -51,6 +51,20 @@ import { trackCliEvent } from './telemetry/integration.mts' import type { CResult } from '../types.mts' import type { RequestInfo, ResponseInfo } from '@socketsecurity/sdk' +// SDK v4 rejects request timeouts outside this range (see the SDK's +// MIN_HTTP_TIMEOUT / MAX_HTTP_TIMEOUT). The SDK does not re-export those +// constants, so the bounds are mirrored here. +const MIN_API_TIMEOUT_MS = 5_000 +const MAX_API_TIMEOUT_MS = 300_000 + +// Clamp a SOCKET_CLI_API_TIMEOUT value into the SDK's accepted range. The SDK +// throws a TypeError on out-of-range timeouts, so an out-of-range user value +// would otherwise make every API call fail; clamping preserves intent (a small +// value maps to the floor, a huge value to the ceiling) while staying valid. +export function clampApiTimeout(timeout: number): number { + return Math.min(Math.max(timeout, MIN_API_TIMEOUT_MS), MAX_API_TIMEOUT_MS) +} + // Lazy-evaluated full CLI user agent for direct (non-SDK) API calls. // Includes the CLI product token, Node.js version, and OS platform/arch. // e.g. "socket/1.1.96 node/v22.0.0 linux/arm64" @@ -229,7 +243,14 @@ export async function setupSdk( ? new HttpAgent() : new HttpsAgent(ca ? { ca } : undefined), ...(apiBaseUrl ? { baseUrl: apiBaseUrl } : {}), - timeout: constants.ENV.SOCKET_CLI_API_TIMEOUT, + // SDK v4 validates the request timeout to be within [5000, 300000] ms and + // throws otherwise. Only forward a truthy timeout; when unset (0) omit it + // so the SDK applies its own default instead of rejecting the value. A + // truthy-but-out-of-range value is clamped into the accepted range rather + // than forwarded, which would make every SDK call throw. + ...(constants.ENV.SOCKET_CLI_API_TIMEOUT + ? { timeout: clampApiTimeout(constants.ENV.SOCKET_CLI_API_TIMEOUT) } + : {}), userAgent: createUserAgentFromPkgJson({ name: constants.ENV.INLINED_SOCKET_CLI_NAME, version: constants.ENV.INLINED_SOCKET_CLI_VERSION, diff --git a/src/utils/sdk.test.mts b/src/utils/sdk.test.mts index d299a90e8f..69a4ae4579 100644 --- a/src/utils/sdk.test.mts +++ b/src/utils/sdk.test.mts @@ -25,7 +25,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import constants from '../constants.mts' -import { getCliUserAgent, getExtraCaCerts, setupSdk } from './sdk.mts' +import { + clampApiTimeout, + getCliUserAgent, + getExtraCaCerts, + setupSdk, +} from './sdk.mts' import type { RequestInfo, ResponseInfo } from '@socketsecurity/sdk' @@ -754,3 +759,21 @@ describe('setupSdk with extra CA certificates', () => { } }) }) + +describe('clampApiTimeout', () => { + it('should leave an in-range timeout unchanged', () => { + expect(clampApiTimeout(5_000)).toBe(5_000) + expect(clampApiTimeout(60_000)).toBe(60_000) + expect(clampApiTimeout(300_000)).toBe(300_000) + }) + + it('should raise a below-minimum timeout to the floor', () => { + expect(clampApiTimeout(1_000)).toBe(5_000) + expect(clampApiTimeout(1)).toBe(5_000) + }) + + it('should lower an above-maximum timeout to the ceiling', () => { + expect(clampApiTimeout(999_999)).toBe(300_000) + expect(clampApiTimeout(300_001)).toBe(300_000) + }) +}) diff --git a/src/utils/socket-package-alert.mts b/src/utils/socket-package-alert.mts index 06850e86ad..6f26f16ae0 100644 --- a/src/utils/socket-package-alert.mts +++ b/src/utils/socket-package-alert.mts @@ -42,7 +42,6 @@ import { getTranslations } from './translations.mts' import type { ALERT_ACTION, - ALERT_TYPE, CompactSocketArtifact, CompactSocketArtifactAlert, CveProps, @@ -190,10 +189,14 @@ export async function addArtifactToAlertsMap( ...getOwn(options, 'filter'), }) as AlertFilter + // `issueRules` is a user-provided YAML map typed as + // `{ [issueName: string]: boolean }`, so key by `string` here. A string + // index signature also keeps the `enabledState[alert.type]` lookup below + // well-typed regardless of how `alert.type` resolves. const enabledState = { __proto__: null, ...socketYml?.issueRules, - } as Partial> + } as unknown as Partial> let sockPkgAlerts: SocketPackageAlert[] = [] for (const alert of artifact.alerts) { @@ -544,7 +547,13 @@ export function logAlertsMap( const severity = alert.raw.severity ?? '' const attributes = [ ...(severity - ? [colors[ALERT_SEVERITY_COLOR[severity]](getSeverityLabel(severity))] + ? [ + colors[ + ALERT_SEVERITY_COLOR[ + severity as keyof typeof ALERT_SEVERITY_COLOR + ] + ](getSeverityLabel(severity)), + ] : []), ...(alert.blocked ? [colors.bold(colors.red('blocked'))] : []), ...(alert.fixable ? ['fixable'] : []),