Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- `socket scan create --reach` and `socket scan reach` take `--reach-advisories` to focus reachability analysis on the GHSA or CVE advisories you care about. Every other advisory is left with unknown reachability.

## [1.10.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.10.0) - 2026-10-22

### Added
Expand Down
1 change: 1 addition & 0 deletions src/commands/ci/handle-ci.mts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function handleCi(autoManifest: boolean): Promise<void> {
reach: {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down
9 changes: 5 additions & 4 deletions src/commands/fix/handle-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@ import { logger } from '@socketsecurity/registry/lib/logger'

import { coanaFix } from './coana-fix.mts'
import { outputFixResult } from './output-fix-result.mts'
import { convertCveToGhsa } from '../../utils/cve-to-ghsa.mts'
import {
CVE_FORMAT_REGEXP,
GHSA_FORMAT_REGEXP,
convertCveToGhsa,
} from '../../utils/cve-to-ghsa.mts'
import { convertPurlToGhsas } from '../../utils/purl-to-ghsa.mts'

import type { FixConfig } from './types.mts'
import type { OutputKind } from '../../types.mts'
import type { Remap } from '@socketsecurity/registry/lib/objects'

const GHSA_FORMAT_REGEXP = /^GHSA-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$/
const CVE_FORMAT_REGEXP = /^CVE-\d{4}-\d{4,}$/

export type HandleFixConfig = Remap<
FixConfig & {
applyFixes: boolean
Expand Down
11 changes: 11 additions & 0 deletions src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from './generate-uv-member-sboms.mts'
import { handleCreateNewScan } from './handle-create-new-scan.mts'
import { outputCreateNewScan } from './output-create-new-scan.mts'
import { resolveReachAdvisories } from './reach-advisories.mts'
import {
DYNAMIC_SBOM_INFERENCE_DESCRIPTION,
excludePathsFlag,
Expand Down Expand Up @@ -523,7 +524,11 @@ async function run(

const reachExcludePaths = cmdFlagValueToArray(cli.flags['reachExcludePaths'])

const reachAdvisoriesRaw = cmdFlagValueToArray(cli.flags['reachAdvisories'])

// Validation helpers for better readability.
const hasReachAdvisories = reachAdvisoriesRaw.length > 0

const hasReachEcosystems = reachEcosystems.length > 0

const hasReachExcludePaths = reachExcludePaths.length > 0
Expand Down Expand Up @@ -559,6 +564,7 @@ async function run(
)

const isUsingAnyReachabilityFlags =
hasReachAdvisories ||
hasReachEcosystems ||
hasReachExcludePaths ||
isUsingAnyBooleanReachFlag ||
Expand Down Expand Up @@ -666,6 +672,10 @@ async function run(
return
}

const reachAdvisories = reach
? await resolveReachAdvisories(reachAdvisoriesRaw)
: []

await handleCreateNewScan({
autoManifest: Boolean(autoManifest),
branchName: branchName as string,
Expand All @@ -685,6 +695,7 @@ async function run(
reach: {
dynamicSbomInference: Boolean(dynamicSbomInference),
excludePaths,
reachAdvisories,
reachAnalysisMemoryLimit,
reachAnalysisTimeout,
reachConcurrency: Number(reachConcurrency),
Expand Down
1 change: 1 addition & 0 deletions src/commands/scan/cmd-scan-create.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ describe('socket scan create', async () => {
--workspace The workspace in the Socket Organization that the repository is in to associate with the full scan.

Reachability Options (when --reach is used)
--reach-advisories List of advisories to run reachability analysis for, as GHSA or CVE IDs, either as a comma separated value or as multiple flags. CVE IDs are converted to GHSA IDs through the GitHub API. Vulnerabilities from other advisories get unknown reachability. Defaults to all advisories.
--reach-analysis-memory-limit The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB.
--reach-analysis-timeout Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly.
--reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available.
Expand Down
5 changes: 5 additions & 0 deletions src/commands/scan/cmd-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'

import { assertValidExcludePaths } from './exclude-paths.mts'
import { handleScanReach } from './handle-scan-reach.mts'
import { resolveReachAdvisories } from './reach-advisories.mts'
import {
DYNAMIC_SBOM_INFERENCE_DESCRIPTION,
excludePathsFlag,
Expand Down Expand Up @@ -200,6 +201,7 @@ async function run(

// Process comma-separated values for isMultiple flags.
const excludePaths = cmdFlagValueToArray(cli.flags['excludePaths'])
const reachAdvisoriesRaw = cmdFlagValueToArray(cli.flags['reachAdvisories'])
const reachEcosystemsRaw = cmdFlagValueToArray(cli.flags['reachEcosystems'])
const reachExcludePaths = cmdFlagValueToArray(cli.flags['reachExcludePaths'])
assertValidExcludePaths(excludePaths)
Expand Down Expand Up @@ -290,6 +292,8 @@ async function run(
return
}

const reachAdvisories = await resolveReachAdvisories(reachAdvisoriesRaw)

await handleScanReach({
cwd,
interactive,
Expand All @@ -299,6 +303,7 @@ async function run(
reachabilityOptions: {
dynamicSbomInference: Boolean(dynamicSbomInference),
excludePaths,
reachAdvisories,
reachAnalysisMemoryLimit,
reachAnalysisTimeout,
reachConcurrency: Number(reachConcurrency),
Expand Down
1 change: 1 addition & 0 deletions src/commands/scan/cmd-scan-reach.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ describe('socket scan reach', async () => {
Reachability Options
--dynamic-sbom-inference For Gradle, sbt, and Maven: generate a Socket facts SBOM (produced directly by each package manager) per independent build root, instead of one synthetic root. The reachability analysis is split per project/module accordingly.
--exclude-paths List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (\`--cwd\` if set), not the reachability target: \`tests\` matches only \`<cwd>/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
--reach-advisories List of advisories to run reachability analysis for, as GHSA or CVE IDs, either as a comma separated value or as multiple flags. CVE IDs are converted to GHSA IDs through the GitHub API. Vulnerabilities from other advisories get unknown reachability. Defaults to all advisories.
--reach-analysis-memory-limit The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB.
--reach-analysis-timeout Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly.
--reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available.
Expand Down
1 change: 1 addition & 0 deletions src/commands/scan/create-scan-from-github.mts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ async function scanOneRepo(
reach: {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down
1 change: 1 addition & 0 deletions src/commands/scan/exclude-paths.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function makeReachOptions(
return {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down
1 change: 1 addition & 0 deletions src/commands/scan/generate-uv-member-sboms.e2e.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ describe('uv member scans with the real uv binary', { timeout: 30_000 }, () => {
reach: {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down
6 changes: 6 additions & 0 deletions src/commands/scan/handle-create-new-scan.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ function createConfig(
reach: {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -563,6 +564,7 @@ describe('handleCreateNewScan excludePaths', () => {
reach: {
dynamicSbomInference: false,
excludePaths: ['tests', 'packages/*'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -627,6 +629,7 @@ describe('handleCreateNewScan excludePaths', () => {
reach: {
dynamicSbomInference: false,
excludePaths: ['apps/api/tests', '**/dist'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -697,6 +700,7 @@ describe('handleCreateNewScan excludePaths', () => {
reach: {
dynamicSbomInference: false,
excludePaths: ['tests'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -757,6 +761,7 @@ describe('handleCreateNewScan excludePaths', () => {
reach: {
dynamicSbomInference: false,
excludePaths: ['apps/api'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -817,6 +822,7 @@ describe('handleCreateNewScan excludePaths', () => {
reach: {
dynamicSbomInference: false,
excludePaths: ['tests'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down
8 changes: 8 additions & 0 deletions src/commands/scan/handle-scan-reach.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ describe('handleScanReach', () => {
const reachabilityOptions = {
dynamicSbomInference: false,
excludePaths: ['tests', 'packages/*'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -171,6 +172,7 @@ describe('handleScanReach', () => {
const reachabilityOptions = {
dynamicSbomInference: false,
excludePaths: ['apps/api/tests', '**/dist'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -233,6 +235,7 @@ describe('handleScanReach', () => {
const reachabilityOptions = {
dynamicSbomInference: false,
excludePaths: ['apps/api'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -281,6 +284,7 @@ describe('handleScanReach', () => {
const reachabilityOptions = {
dynamicSbomInference: false,
excludePaths: ['tests'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -333,6 +337,7 @@ describe('handleScanReach', () => {
const reachabilityOptions = {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -370,6 +375,7 @@ describe('handleScanReach', () => {
const reachabilityOptions = {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -420,6 +426,7 @@ describe('handleScanReach', () => {
const reachabilityOptions = {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down Expand Up @@ -473,6 +480,7 @@ describe('handleScanReach', () => {
const baseReachabilityOptions = {
dynamicSbomInference: true,
excludePaths: ['vendor/**'],
reachAdvisories: [],
reachAnalysisMemoryLimit: '8192',
reachAnalysisTimeout: '',
reachConcurrency: 1,
Expand Down
5 changes: 5 additions & 0 deletions src/commands/scan/perform-reachability-analysis.mts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import type { StdioOptions } from 'node:child_process'
export type ReachabilityOptions = {
dynamicSbomInference: boolean
excludePaths: string[]
// GHSA IDs. Empty means all advisories.
reachAdvisories: string[]
reachAnalysisMemoryLimit: string
reachAnalysisTimeout: string
reachConcurrency: number
Expand Down Expand Up @@ -254,6 +256,9 @@ export async function performReachabilityAnalysis(
...(reachabilityOptions.reachEcosystems.length
? ['--purl-types', ...reachabilityOptions.reachEcosystems]
: []),
...(reachabilityOptions.reachAdvisories.length
? ['--ghsas', ...reachabilityOptions.reachAdvisories]
: []),
...(reachabilityOptions.reachExcludePaths.length
? ['--exclude-dirs', ...reachabilityOptions.reachExcludePaths]
: []),
Expand Down
19 changes: 18 additions & 1 deletion src/commands/scan/perform-reachability-analysis.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function makeReachabilityOptions(): ReachabilityOptions {
return {
dynamicSbomInference: false,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '',
reachAnalysisTimeout: '',
reachConcurrency: 0,
Expand Down Expand Up @@ -266,7 +267,7 @@ describe('performReachabilityAnalysis manifests tar hash', () => {
})
})

describe('performReachabilityAnalysis timeout/memory forwarding', () => {
describe('performReachabilityAnalysis flag forwarding', () => {
let scanCwd: string

beforeEach(() => {
Expand Down Expand Up @@ -325,6 +326,22 @@ describe('performReachabilityAnalysis timeout/memory forwarding', () => {
expect(args).not.toContain('--analysis-timeout')
expect(args).not.toContain('--memory-limit')
})

it('passes --reach-advisories to Coana as --ghsas', async () => {
const args = await coanaArgsFor({
reachAdvisories: ['GHSA-35jh-r3h4-6jhm', 'GHSA-r5fr-rjxr-66jc'],
})
const ghsasIndex = args.indexOf('--ghsas')
expect(args.slice(ghsasIndex + 1, ghsasIndex + 3)).toEqual([
'GHSA-35jh-r3h4-6jhm',
'GHSA-r5fr-rjxr-66jc',
])
})

it('omits --ghsas when no advisories are given', async () => {
const args = await coanaArgsFor({ reachAdvisories: [] })
expect(args).not.toContain('--ghsas')
})
})

describe('performReachabilityAnalysis --maven-use-only-socket-facts gating', () => {
Expand Down
56 changes: 56 additions & 0 deletions src/commands/scan/reach-advisories.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { arrayUnique } from '@socketsecurity/registry/lib/arrays'

import {
CVE_FORMAT_REGEXP,
GHSA_FORMAT_REGEXP,
convertCveToGhsa,
} from '../../utils/cve-to-ghsa.mts'
import { InputError } from '../../utils/errors.mts'

// GitHub publishes GHSA IDs with a lowercase body and CVE IDs uppercase.
function normalizeAdvisoryId(id: string): string {
const trimmed = id.trim()
return /^ghsa-/i.test(trimmed)
? `GHSA-${trimmed.slice(5).toLowerCase()}`
: trimmed.toUpperCase()
}

/**
* Resolves the --reach-advisories IDs to GHSA IDs, converting CVE IDs through
* the GitHub API. Any ID that cannot be resolved fails the command, since
* dropping it would leave that advisory without reachability analysis.
*/
export async function resolveReachAdvisories(
ids: readonly string[],
): Promise<string[]> {
const normalizedIds = arrayUnique(ids.map(normalizeAdvisoryId))
const invalidIds = normalizedIds.filter(
id => !GHSA_FORMAT_REGEXP.test(id) && !CVE_FORMAT_REGEXP.test(id),
)
if (invalidIds.length) {
throw new InputError(
`Invalid --reach-advisories IDs: ${invalidIds.join(', ')}. Use GHSA IDs (GHSA-xxxx-xxxx-xxxx) or CVE IDs (CVE-YYYY-NNNN).`,
)
}
const ghsas: string[] = []
const errors: string[] = []
for (const id of normalizedIds) {
if (GHSA_FORMAT_REGEXP.test(id)) {
ghsas.push(id)
continue
}
// eslint-disable-next-line no-await-in-loop
const result = await convertCveToGhsa(id)
if (result.ok) {
ghsas.push(result.data)
} else {
errors.push(`${id}: ${result.message}`)
}
}
if (errors.length) {
throw new InputError(
`Could not resolve --reach-advisories IDs to GHSA IDs:\n${errors.map(e => ` - ${e}`).join('\n')}`,
)
}
return arrayUnique(ghsas)
}
Loading
Loading