Skip to content
Open
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
211 changes: 211 additions & 0 deletions src/command/manifest/bazel/bazel-socket-json-trust.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/**
* Socket.json settings that pick what `socket manifest bazel` runs, or where
* it writes outside the project, need `--trust-socket-json`.
*/
import path from 'node:path'

import { SOCKET_JSON } from '../../../constants/socket.mts'
import { isPathWithinRoot } from '../../../util/trusted-executable.mts'
import {
canonicalizeForContainment,
readBuildToolBin,
TRUST_SOCKET_JSON_FLAG,
} from '../manifest-build-trust.mts'

import type { CResult } from '../../../types.mts'
import type { SocketJson } from '../../../util/socket/json.mts'

export type BazelSocketJsonConfig = NonNullable<
NonNullable<NonNullable<SocketJson['defaults']>['manifest']>['bazel']
>

export type BazelManifestCliSettings = {
bazel: unknown
bazelFlags: unknown
bazelOutputBase: unknown
bazelRc: unknown
out: unknown
}

export type BazelManifestSettings = {
bazel: string | undefined
bazelFlags: string
bazelOutputBase: string | undefined
bazelRc: string | undefined
out: string
socketJsonDefaults: Array<{ flag: string; value: string }>
}

export type BazelManifestSettingTally = {
refusedFields: string[]
socketJsonDefaults: BazelManifestSettings['socketJsonDefaults']
trustSocketJson: boolean
}

/**
* A command-line value always wins. A socket.json value is recorded as refused
* unless the trust flag is set.
*/
export function readBazelManifestSetting(
tally: BazelManifestSettingTally,
{
cliValue,
field,
flag,
socketJsonValue,
}: {
cliValue: unknown
field: string
flag: string
socketJsonValue: unknown
},
): string | undefined {
const fromCli = readBuildToolBin(cliValue)
if (fromCli) {
return fromCli
}
const fromSocketJson = readBuildToolBin(socketJsonValue)
if (!fromSocketJson) {
return undefined
}
if (!tally.trustSocketJson) {
tally.refusedFields.push(field)
return undefined
}
tally.socketJsonDefaults.push({ flag, value: fromSocketJson })
return fromSocketJson
}

export function resolveBazelManifestOut({
cliOut,
cwd,
socketJson,
trustSocketJson,
}: {
cliOut: unknown
cwd: string
socketJson: SocketJson | undefined
trustSocketJson: boolean
}): CResult<{ fromSocketJson: boolean; path: string }> {
const fromCli = readBuildToolBin(cliOut)
if (fromCli) {
return { ok: true, data: { fromSocketJson: false, path: fromCli } }
}
const socketJsonOut = readBuildToolBin(
socketJson?.defaults?.manifest?.bazel?.out,
)
if (!socketJsonOut) {
return {
ok: true,
data: {
fromSocketJson: false,
path: path.join(cwd, '.socket', 'bazel-manifests'),
},
}
}
const resolvedOut = path.resolve(cwd, socketJsonOut)
if (
!trustSocketJson &&
!isPathWithinRoot(
canonicalizeForContainment(cwd),
canonicalizeForContainment(resolvedOut),
)
) {
return {
ok: false,
message: `Refused a bazel output path chosen by ${SOCKET_JSON}`,
cause: [
`${SOCKET_JSON} in ${cwd} sets defaults.manifest.bazel.out.`,
`Saw \`${socketJsonOut}\` (resolves to ${resolvedOut}), wanted a path inside ${cwd}.`,
'The CLI writes generated manifests there, so a path outside the project would let a scanned repository choose where the CLI writes.',
`Fix: pass \`--out=${socketJsonOut}\` on the command line if you trust it, or re-run with ${TRUST_SOCKET_JSON_FLAG} to honor ${SOCKET_JSON} in this checkout.`,
].join('\n'),
}
}
return { ok: true, data: { fromSocketJson: true, path: resolvedOut } }
}

export function resolveBazelManifestSettings({
cli,
cwd,
socketJson,
trustSocketJson,
}: {
cli: BazelManifestCliSettings
cwd: string
socketJson: SocketJson | undefined
trustSocketJson: boolean
}): CResult<BazelManifestSettings> {
const bazelConfig: BazelSocketJsonConfig =
socketJson?.defaults?.manifest?.bazel ?? {}
const tally: BazelManifestSettingTally = {
refusedFields: [],
socketJsonDefaults: [],
trustSocketJson,
}

const bazel = readBazelManifestSetting(tally, {
cliValue: cli.bazel,
field: bazelConfig.bazel
? 'defaults.manifest.bazel.bazel'
: 'defaults.manifest.bazel.bin',
flag: '--bazel',
socketJsonValue: bazelConfig.bazel ?? bazelConfig.bin,
})
const bazelFlags = readBazelManifestSetting(tally, {
cliValue: cli.bazelFlags,
field: 'defaults.manifest.bazel.bazelFlags',
flag: '--bazel-flags',
socketJsonValue: bazelConfig.bazelFlags,
})
const bazelRc = readBazelManifestSetting(tally, {
cliValue: cli.bazelRc,
field: 'defaults.manifest.bazel.bazelRc',
flag: '--bazel-rc',
socketJsonValue: bazelConfig.bazelRc,
})
const bazelOutputBase = readBazelManifestSetting(tally, {
cliValue: cli.bazelOutputBase,
field: 'defaults.manifest.bazel.bazelOutputBase',
flag: '--bazel-output-base',
socketJsonValue: bazelConfig.bazelOutputBase,
})

if (tally.refusedFields.length) {
return {
ok: false,
message: `Refused bazel settings chosen by ${SOCKET_JSON}`,
cause: [
`${SOCKET_JSON} in ${cwd} sets ${tally.refusedFields.join(', ')}.`,
'Saw repository-supplied bazel execution settings, wanted none.',
'These values pick the bazel binary and the options it runs with, and a scanned repository controls its own socket.json.',
`Fix: pass --bazel, --bazel-flags, --bazel-rc, or --bazel-output-base on the command line, or re-run with ${TRUST_SOCKET_JSON_FLAG} to honor ${SOCKET_JSON} in this checkout.`,
].join('\n'),
}
}

const out = resolveBazelManifestOut({
cliOut: cli.out,
cwd,
socketJson,
trustSocketJson,
})
if (!out.ok) {
return out
}
if (out.data.fromSocketJson) {
tally.socketJsonDefaults.push({ flag: '--out', value: out.data.path })
}

return {
ok: true,
data: {
bazel,
bazelFlags: bazelFlags ?? '',
bazelOutputBase,
bazelRc,
out: out.data.path,
socketJsonDefaults: tally.socketJsonDefaults,
},
}
}
71 changes: 30 additions & 41 deletions src/command/manifest/bazel/cmd-manifest-bazel.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import path from 'node:path'
import { debugDirNs } from '@socketsecurity/lib-stable/debug/output'
import { getDefaultLogger } from '@socketsecurity/lib-stable/logger/default'

import { resolveBazelManifestSettings } from './bazel-socket-json-trust.mts'
import { extractBazelToMaven } from './extract_bazel_to_maven.mts'
import { extractBazelToPypi } from './extract_bazel_to_pypi.mts'
import { outputManifest } from '../output-manifest.mts'
import { SOCKET_JSON } from '../../../constants/socket.mts'
import { defineFlags } from '../../../meow.mts'
import { commonFlags, outputFlags } from '../../../flags.mts'
Expand Down Expand Up @@ -77,6 +79,11 @@ const config = {
description:
'Per-hub bazel cquery timeout in milliseconds; default: 120000',
},
trustSocketJson: {
type: 'boolean',
default: false,
description: `Run the bazel binary and options declared in ${SOCKET_JSON}. Off by default because the scanned repository controls that file.`,
},
verbose: {
type: 'boolean',
description:
Expand All @@ -99,6 +106,12 @@ const config = {
--ecosystem may be repeated to select which ecosystems to extract.
When omitted, Maven is generated by default. PyPI is explicit opt-in.

A ${SOCKET_JSON} that sets \`bazel\`/\`bin\`, \`bazelFlags\`, \`bazelRc\`, or
\`bazelOutputBase\`, or an \`out\` outside the project, is refused unless
you pass --trust-socket-json: those values choose what gets executed and
the repository being scanned owns that file. Pass the matching flags
yourself to override the defaults without trusting ${SOCKET_JSON}.

Note: this command generates dependency manifests for Bazel workspaces.
It does not run reachability analysis.

Expand Down Expand Up @@ -272,52 +285,28 @@ export async function run(
[`${SOCKET_JSON} bazel defaults`]: sockJson.defaults?.manifest?.bazel,
})

let { bazel, bazelFlags, bazelOutputBase, bazelRc, out, verbose } = cli.flags
const settings = resolveBazelManifestSettings({
cli: cli.flags,
cwd,
socketJson: sockJson,
trustSocketJson: cli.flags['trustSocketJson'],
})
if (!settings.ok) {
await outputManifest(settings, outputKind, '-')
return
}
for (const { flag, value } of settings.data.socketJsonDefaults) {
logger.info(`Using default ${flag} from ${SOCKET_JSON}:`, value)
}
// An undefined bazel leaves the bazelisk/bazel PATH lookup to
// resolveBazelBinary.
const { bazel, bazelFlags, bazelOutputBase, bazelRc, out } = settings.data
let { verbose } = cli.flags
let perRepoTimeout = cli.flags['perRepoTimeout'] as number | undefined

const resolved = resolveBazelDefaults()

function resolveBazelDefaults() {
// Set defaults for any flag/arg that is not given. Check socket.json first.
if (!bazel) {
const defaultBazel = defaults.bazel ?? defaults.bin
if (defaultBazel) {
bazel = defaultBazel
logger.info(`Using default --bazel from ${SOCKET_JSON}:`, bazel)
}
// Otherwise leave undefined; resolveBazelBinary performs the PATH
// lookup for bazelisk/bazel.
}
if (!bazelFlags) {
if (defaults.bazelFlags) {
bazelFlags = defaults.bazelFlags
logger.info(
`Using default --bazel-flags from ${SOCKET_JSON}:`,
bazelFlags,
)
} else {
bazelFlags = ''
}
}
if (!bazelOutputBase && defaults.bazelOutputBase) {
bazelOutputBase = defaults.bazelOutputBase
logger.info(
`Using default --bazel-output-base from ${SOCKET_JSON}:`,
bazelOutputBase,
)
}
if (!bazelRc && defaults.bazelRc) {
bazelRc = defaults.bazelRc
logger.info(`Using default --bazel-rc from ${SOCKET_JSON}:`, bazelRc)
}
if (!out) {
if (defaults.out) {
out = defaults.out
logger.info(`Using default --out from ${SOCKET_JSON}:`, out)
} else {
out = path.join(cwd, '.socket', 'bazel-manifests')
}
}
verbose = resolveManifestDefault(
verbose,
defaults.verbose,
Expand Down
Loading
Loading