From dec47bfce8883bd9b0ebeadef46e5716c2a2ab44 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Fri, 19 Jun 2026 16:35:48 +0200 Subject: [PATCH 1/3] feat(manifest): add `socket manifest maven` (1.1.124, Coana 15.5.5) Add a `socket manifest maven` command that generates a Socket facts file (`.socket.facts.json`) from a Maven `pom.xml` project by delegating to the Coana CLI's `manifest maven` command, mirroring the existing gradle/sbt facts flows. Includes pom.xml auto-detection, `socket manifest auto` wiring, the `socket manifest setup` configurator, socket.json defaults, and `--maven-opts` / `--bin` pass-through. Bump Coana CLI to 15.5.5, which adds the `manifest maven` command this delegates to. --- CHANGELOG.md | 8 + package.json | 4 +- pnpm-lock.yaml | 10 +- src/commands/manifest/README.md | 7 + src/commands/manifest/cmd-manifest-maven.mts | 235 ++++++++++++++++++ .../manifest/cmd-manifest-maven.test.mts | 84 +++++++ src/commands/manifest/cmd-manifest.mts | 4 +- src/commands/manifest/cmd-manifest.test.mts | 1 + .../manifest/coana-manifest-facts.mts | 12 +- .../manifest/convert-maven-to-facts.mts | 36 +++ .../manifest/detect-manifest-actions.mts | 13 + .../manifest/generate_auto_manifest.mts | 21 ++ .../manifest/setup-manifest-config.mts | 58 +++++ src/utils/socket-json.mts | 9 + 14 files changed, 488 insertions(+), 14 deletions(-) create mode 100644 src/commands/manifest/cmd-manifest-maven.mts create mode 100644 src/commands/manifest/cmd-manifest-maven.test.mts create mode 100644 src/commands/manifest/convert-maven-to-facts.mts diff --git a/CHANGELOG.md b/CHANGELOG.md index 58866309c..97abb5fb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ 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/). +## [1.1.124](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.124) - 2026-06-19 + +### Added +- New `socket manifest maven` command generates a Socket facts file (`.socket.facts.json`) directly from a Maven `pom.xml` project. Like the Gradle and sbt generators, it auto-detects your project, plugs into `socket manifest auto` and the `socket manifest setup` configurator, and accepts `--maven-opts` to pass options through to Maven (e.g. `--maven-opts="-P release -s settings.xml"`), plus `--bin` to point at a wrapper such as `./mvnw`. + +### Changed +- Updated the Coana CLI to v `15.5.5`. + ## [1.1.123](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.123) - 2026-06-18 ### Added diff --git a/package.json b/package.json index bb4cd8449..fe700aae7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.123", + "version": "1.1.124", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT", @@ -96,7 +96,7 @@ "@babel/preset-typescript": "7.27.1", "@babel/runtime": "7.28.4", "@biomejs/biome": "2.2.4", - "@coana-tech/cli": "15.5.0", + "@coana-tech/cli": "15.5.5", "@cyclonedx/cdxgen": "12.1.2", "@dotenvx/dotenvx": "1.49.0", "@eslint/compat": "1.3.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 08379840d..d588dc211 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -128,8 +128,8 @@ importers: specifier: 2.2.4 version: 2.2.4 '@coana-tech/cli': - specifier: 15.5.0 - version: 15.5.0 + specifier: 15.5.5 + version: 15.5.5 '@cyclonedx/cdxgen': specifier: 12.1.2 version: 12.1.2 @@ -749,8 +749,8 @@ packages: resolution: {integrity: sha512-hAs5PPKPCQ3/Nha+1fo4A4/gL85fIfxZwHPehsjCJ+BhQH2/yw6/xReuaPA/RfNQr6iz1PcD7BZcE3ctyyl3EA==} cpu: [x64] - '@coana-tech/cli@15.5.0': - resolution: {integrity: sha512-CpFOZ2I5Fb/GM5YPqBwlIN5YjwNBlHPAVpDfO9E6rEk12fTdJ1tVH6brOPmzrpuuRvxQx97VXFpYwjrKCSuHNA==} + '@coana-tech/cli@15.5.5': + resolution: {integrity: sha512-eFZ1q1i7Xr8gEA80OfmzIoQXkDjrF3AUeRYaOvibVW6bJMJfDTBmxRkYtks6sloGgbNR9X/8Lwy4V+Z+D6llpA==} hasBin: true '@colors/colors@1.5.0': @@ -5385,7 +5385,7 @@ snapshots: '@cdxgen/cdxgen-plugins-bin@2.0.2': optional: true - '@coana-tech/cli@15.5.0': {} + '@coana-tech/cli@15.5.5': {} '@colors/colors@1.5.0': optional: true diff --git a/src/commands/manifest/README.md b/src/commands/manifest/README.md index 0798df74b..0f54b10a7 100644 --- a/src/commands/manifest/README.md +++ b/src/commands/manifest/README.md @@ -151,6 +151,13 @@ scala flows. Uses Gradle to generate a manifest file (`pom.xml`) for a Kotlin project; the underlying flow is identical to the gradle subcommand. +## socket manifest maven [beta] + +Generates a Socket facts file (`.socket.facts.json`) from a Maven `pom.xml` +project, using `mvn` (override with `--bin`, e.g. a project `./mvnw` wrapper). +Pass extra options through to maven with `--maven-opts` (e.g. +`--maven-opts="-P release -s settings.xml"`). + ## socket manifest scala [beta] Generates a manifest file (`pom.xml`) from Scala's `build.sbt` file. diff --git a/src/commands/manifest/cmd-manifest-maven.mts b/src/commands/manifest/cmd-manifest-maven.mts new file mode 100644 index 000000000..7db110028 --- /dev/null +++ b/src/commands/manifest/cmd-manifest-maven.mts @@ -0,0 +1,235 @@ +import path from 'node:path' + +import { debugFn } from '@socketsecurity/registry/lib/debug' +import { logger } from '@socketsecurity/registry/lib/logger' + +import { convertMavenToFacts } from './convert-maven-to-facts.mts' +import constants, { SOCKET_JSON } from '../../constants.mts' +import { commonFlags } from '../../flags.mts' +import { checkCommandInput } from '../../utils/check-input.mts' +import { getOutputKind } from '../../utils/get-output-kind.mts' +import { meowOrExit } from '../../utils/meow-with-subcommands.mts' +import { getFlagListOutput } from '../../utils/output-formatting.mts' +import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' + +import type { + CliCommandConfig, + CliCommandContext, +} from '../../utils/meow-with-subcommands.mts' + +const config: CliCommandConfig = { + commandName: 'maven', + description: + '[beta] Generate a Socket facts file from a Maven `pom.xml` project', + hidden: false, + flags: { + ...commonFlags, + bin: { + type: 'string', + description: 'Location of the maven binary to use, default: mvn on PATH', + }, + includeConfigs: { + type: 'string', + description: + 'Comma-separated glob patterns matched against Maven dependency scopes (case-sensitive, `*` and `?` wildcards). Only scopes matching at least one pattern are resolved. e.g. `compile,runtime`. Default: every scope', + }, + excludeConfigs: { + type: 'string', + description: + 'Comma-separated glob patterns; Maven scopes matching any pattern are skipped (applied after --include-configs)', + }, + ignoreUnresolved: { + type: 'boolean', + description: + 'Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)', + }, + mavenOpts: { + type: 'string', + description: + 'Additional options to pass on to maven, e.g. `-P -s `', + }, + verbose: { + type: 'boolean', + description: 'Print debug messages', + }, + }, + help: (command, config) => ` + Usage + $ ${command} [options] [CWD=.] + + Options + ${getFlagListOutput(config.flags)} + + Emits a single \`.socket.facts.json\` describing the resolved dependency + graph of your Maven project, using maven (\`mvn\` on PATH by default). It + reads dependency metadata only and never downloads artifacts; an unresolved + dependency is a fatal error. You can pass --include-configs / + --exclude-configs (comma-separated glob patterns) to control which Maven + scopes are resolved (e.g. --include-configs=\`compile,runtime\`), and + --ignore-unresolved to warn on unresolved dependencies instead of failing. + + You can specify --bin to override the path to the \`mvn\` binary to invoke + (e.g. a project \`./mvnw\` wrapper), and --maven-opts to pass extra options + through to maven (e.g. \`-P -s \`). + + Support is beta. Please report issues or give us feedback on what's missing. + + Examples + + $ ${command} . + $ ${command} --bin=./mvnw . + $ ${command} --maven-opts="-P release" . + `, +} + +export const cmdManifestMaven = { + description: config.description, + hidden: config.hidden, + run, +} + +async function run( + argv: string[] | readonly string[], + importMeta: ImportMeta, + { parentName }: CliCommandContext, +): Promise { + const cli = meowOrExit({ + argv, + config, + importMeta, + parentName, + }) + + const { json = false, markdown = false } = cli.flags + + const dryRun = !!cli.flags['dryRun'] + + // TODO: Implement json/md further. + const outputKind = getOutputKind(json, markdown) + + let [cwd = '.'] = cli.input + // Note: path.resolve vs .join: + // If given path is absolute then cwd should not affect it. + cwd = path.resolve(process.cwd(), cwd) + + const sockJson = readOrDefaultSocketJson(cwd) + + debugFn( + 'inspect', + `override: ${SOCKET_JSON} maven`, + sockJson?.defaults?.manifest?.maven, + ) + + let { + bin, + excludeConfigs, + ignoreUnresolved, + includeConfigs, + mavenOpts, + verbose, + } = cli.flags + + // Set defaults for any flag/arg that is not given. Check socket.json first. + if (!bin) { + if (sockJson.defaults?.manifest?.maven?.bin) { + bin = sockJson.defaults?.manifest?.maven?.bin + logger.info(`Using default --bin from ${SOCKET_JSON}:`, bin) + } else { + bin = 'mvn' + } + } + if (!mavenOpts) { + if (sockJson.defaults?.manifest?.maven?.mavenOpts) { + mavenOpts = sockJson.defaults?.manifest?.maven?.mavenOpts + logger.info(`Using default --maven-opts from ${SOCKET_JSON}:`, mavenOpts) + } else { + mavenOpts = '' + } + } + if (includeConfigs === undefined) { + if (sockJson.defaults?.manifest?.maven?.includeConfigs !== undefined) { + includeConfigs = sockJson.defaults?.manifest?.maven?.includeConfigs + logger.info( + `Using default --include-configs from ${SOCKET_JSON}:`, + includeConfigs, + ) + } else { + includeConfigs = '' + } + } + if (excludeConfigs === undefined) { + if (sockJson.defaults?.manifest?.maven?.excludeConfigs !== undefined) { + excludeConfigs = sockJson.defaults?.manifest?.maven?.excludeConfigs + logger.info( + `Using default --exclude-configs from ${SOCKET_JSON}:`, + excludeConfigs, + ) + } else { + excludeConfigs = '' + } + } + if (ignoreUnresolved === undefined) { + if (sockJson.defaults?.manifest?.maven?.ignoreUnresolved !== undefined) { + ignoreUnresolved = sockJson.defaults?.manifest?.maven?.ignoreUnresolved + logger.info( + `Using default --ignore-unresolved from ${SOCKET_JSON}:`, + ignoreUnresolved, + ) + } else { + ignoreUnresolved = false + } + } + if (verbose === undefined) { + if (sockJson.defaults?.manifest?.maven?.verbose !== undefined) { + verbose = sockJson.defaults?.manifest?.maven?.verbose + logger.info(`Using default --verbose from ${SOCKET_JSON}:`, verbose) + } else { + verbose = false + } + } + + if (verbose) { + logger.group('- ', parentName, config.commandName, ':') + logger.group('- flags:', cli.flags) + logger.groupEnd() + logger.log('- input:', cli.input) + logger.groupEnd() + } + + const wasValidInput = checkCommandInput(outputKind, { + nook: true, + test: cli.input.length <= 1, + message: 'Can only accept one DIR (make sure to escape spaces!)', + fail: 'received ' + cli.input.length, + }) + if (!wasValidInput) { + return + } + + if (verbose) { + logger.group() + logger.info('- cwd:', cwd) + logger.info('- maven bin:', bin) + logger.groupEnd() + } + + if (dryRun) { + logger.log(constants.DRY_RUN_BAILING_NOW) + return + } + + const parsedMavenOpts = String(mavenOpts || '') + .split(' ') + .map(s => s.trim()) + .filter(Boolean) + + await convertMavenToFacts({ + bin: String(bin), + cwd, + excludeConfigs: String(excludeConfigs || ''), + ignoreUnresolved: Boolean(ignoreUnresolved), + includeConfigs: String(includeConfigs || ''), + mavenOpts: parsedMavenOpts, + verbose: Boolean(verbose), + }) +} diff --git a/src/commands/manifest/cmd-manifest-maven.test.mts b/src/commands/manifest/cmd-manifest-maven.test.mts new file mode 100644 index 000000000..c4dc818b3 --- /dev/null +++ b/src/commands/manifest/cmd-manifest-maven.test.mts @@ -0,0 +1,84 @@ +import { describe, expect } from 'vitest' + +import constants, { + FLAG_CONFIG, + FLAG_DRY_RUN, + FLAG_HELP, +} from '../../../src/constants.mts' +import { cmdit, spawnSocketCli } from '../../../test/utils.mts' + +describe('socket manifest maven', async () => { + const { binCliPath } = constants + + cmdit( + ['manifest', 'maven', FLAG_HELP, FLAG_CONFIG, '{}'], + `should support ${FLAG_HELP}`, + async cmd => { + const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) + expect(stdout).toMatchInlineSnapshot(` + "[beta] Generate a Socket facts file from a Maven \`pom.xml\` project + + Usage + $ socket manifest maven [options] [CWD=.] + + Options + --bin Location of the maven binary to use, default: mvn on PATH + --exclude-configs Comma-separated glob patterns; Maven scopes matching any pattern are skipped (applied after --include-configs) + --ignore-unresolved Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) + --include-configs Comma-separated glob patterns matched against Maven dependency scopes (case-sensitive, \`*\` and \`?\` wildcards). Only scopes matching at least one pattern are resolved. e.g. \`compile,runtime\`. Default: every scope + --maven-opts Additional options to pass on to maven, e.g. \`-P -s \` + --verbose Print debug messages + + Emits a single \`.socket.facts.json\` describing the resolved dependency + graph of your Maven project, using maven (\`mvn\` on PATH by default). It + reads dependency metadata only and never downloads artifacts; an unresolved + dependency is a fatal error. You can pass --include-configs / + --exclude-configs (comma-separated glob patterns) to control which Maven + scopes are resolved (e.g. --include-configs=\`compile,runtime\`), and + --ignore-unresolved to warn on unresolved dependencies instead of failing. + + You can specify --bin to override the path to the \`mvn\` binary to invoke + (e.g. a project \`./mvnw\` wrapper), and --maven-opts to pass extra options + through to maven (e.g. \`-P -s \`). + + Support is beta. Please report issues or give us feedback on what's missing. + + Examples + + $ socket manifest maven . + $ socket manifest maven --bin=./mvnw . + $ socket manifest maven --maven-opts="-P release" ." + `) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | CLI: + |__ | * | _| '_| -_| _| | token: , org: + |_____|___|___|_,_|___|_|.dev | Command: \`socket manifest maven\`, cwd: " + `) + + expect(code, 'explicit help should exit with code 0').toBe(0) + expect(stderr, 'banner includes base command').toContain( + '`socket manifest maven`', + ) + }, + ) + + cmdit( + ['manifest', 'maven', FLAG_DRY_RUN, FLAG_CONFIG, '{}'], + 'should require args with just dry-run', + async cmd => { + const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) + expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | CLI: + |__ | * | _| '_| -_| _| | token: , org: + |_____|___|___|_,_|___|_|.dev | Command: \`socket manifest maven\`, cwd: " + `) + + expect(code, 'dry-run should exit with code 0 if input ok').toBe(0) + }, + ) +}) diff --git a/src/commands/manifest/cmd-manifest.mts b/src/commands/manifest/cmd-manifest.mts index cd617dd79..518cdfddd 100644 --- a/src/commands/manifest/cmd-manifest.mts +++ b/src/commands/manifest/cmd-manifest.mts @@ -4,6 +4,7 @@ import { cmdManifestCdxgen } from './cmd-manifest-cdxgen.mts' import { cmdManifestConda } from './cmd-manifest-conda.mts' import { cmdManifestGradle } from './cmd-manifest-gradle.mts' import { cmdManifestKotlin } from './cmd-manifest-kotlin.mts' +import { cmdManifestMaven } from './cmd-manifest-maven.mts' import { cmdManifestScala } from './cmd-manifest-scala.mts' import { cmdManifestSetup } from './cmd-manifest-setup.mts' import { REQUIREMENTS_TXT } from '../../constants.mts' @@ -39,7 +40,7 @@ const config: CliCommandConfig = { per language. Currently supported language: bazel [beta], gradle [beta], kotlin (through - gradle) [beta], scala [beta]. + gradle) [beta], maven [beta], scala [beta]. Examples @@ -74,6 +75,7 @@ async function run( conda: cmdManifestConda, gradle: cmdManifestGradle, kotlin: cmdManifestKotlin, + maven: cmdManifestMaven, scala: cmdManifestScala, setup: cmdManifestSetup, }, diff --git a/src/commands/manifest/cmd-manifest.test.mts b/src/commands/manifest/cmd-manifest.test.mts index 604eb451c..93c264770 100644 --- a/src/commands/manifest/cmd-manifest.test.mts +++ b/src/commands/manifest/cmd-manifest.test.mts @@ -29,6 +29,7 @@ describe('socket manifest', async () => { conda [beta] Convert a Conda environment.yml file to a python requirements.txt gradle [beta] Generate a Socket facts file (or \`pom.xml\` with --pom) for a Gradle/Java/Kotlin/etc project kotlin [beta] Generate a Socket facts file (or \`pom.xml\` with --pom) for a Kotlin project + maven [beta] Generate a Socket facts file from a Maven \`pom.xml\` project scala [beta] Generate a Socket facts file (or \`pom.xml\` with --pom) from a Scala \`build.sbt\` project setup Start interactive configurator to customize default flag values for \`socket manifest\` in this dir diff --git a/src/commands/manifest/coana-manifest-facts.mts b/src/commands/manifest/coana-manifest-facts.mts index c77cde292..47ac6fb72 100644 --- a/src/commands/manifest/coana-manifest-facts.mts +++ b/src/commands/manifest/coana-manifest-facts.mts @@ -17,10 +17,10 @@ import { spawnCoanaDlx } from '../../utils/dlx.mts' // facts file. // // `spawnCoanaDlx` resolves the Coana CLI via dlx (or a local build when -// `SOCKET_CLI_COANA_LOCAL_PATH` is set). `bin` (the gradle/sbt executable) is -// always resolved by the caller to a concrete default (`/gradlew`, or -// `sbt` on PATH) before we get here, so it is forwarded verbatim; the empty -// guard below is just a cheap safeguard against passing `--bin ''`. +// `SOCKET_CLI_COANA_LOCAL_PATH` is set). `bin` (the gradle/maven/sbt executable) +// is always resolved by the caller to a concrete default (`/gradlew`, or +// `mvn`/`sbt` on PATH) before we get here, so it is forwarded verbatim; the +// empty guard below is just a cheap safeguard against passing `--bin ''`. export async function runCoanaManifestFacts({ bin, buildOpts, @@ -34,9 +34,9 @@ export async function runCoanaManifestFacts({ }: { bin: string buildOpts: string[] - buildOptsFlag: '--gradle-opts' | '--sbt-opts' + buildOptsFlag: '--gradle-opts' | '--maven-opts' | '--sbt-opts' cwd: string - ecosystem: 'gradle' | 'sbt' + ecosystem: 'gradle' | 'maven' | 'sbt' excludeConfigs: string ignoreUnresolved: boolean includeConfigs: string diff --git a/src/commands/manifest/convert-maven-to-facts.mts b/src/commands/manifest/convert-maven-to-facts.mts new file mode 100644 index 000000000..5255321ce --- /dev/null +++ b/src/commands/manifest/convert-maven-to-facts.mts @@ -0,0 +1,36 @@ +import { runCoanaManifestFacts } from './coana-manifest-facts.mts' + +// Generates a `.socket.facts.json` for a Maven project by delegating to the +// Coana CLI's `manifest maven` command (which owns the Maven plugin that +// resolves the dependency graph). socket-cli no longer runs maven itself; an +// explicit `bin` is forwarded as `--bin`, otherwise Coana defaults to `mvn` on +// PATH. +export async function convertMavenToFacts({ + bin, + cwd, + excludeConfigs, + ignoreUnresolved, + includeConfigs, + mavenOpts, + verbose, +}: { + bin: string + cwd: string + excludeConfigs: string + ignoreUnresolved: boolean + includeConfigs: string + mavenOpts: string[] + verbose: boolean +}): Promise { + await runCoanaManifestFacts({ + bin, + buildOpts: mavenOpts, + buildOptsFlag: '--maven-opts', + cwd, + ecosystem: 'maven', + excludeConfigs, + ignoreUnresolved, + includeConfigs, + verbose, + }) +} diff --git a/src/commands/manifest/detect-manifest-actions.mts b/src/commands/manifest/detect-manifest-actions.mts index b03309bd0..7be3f7330 100644 --- a/src/commands/manifest/detect-manifest-actions.mts +++ b/src/commands/manifest/detect-manifest-actions.mts @@ -20,6 +20,7 @@ export interface GeneratableManifests { count: number conda: boolean gradle: boolean + maven: boolean sbt: boolean } @@ -35,6 +36,7 @@ export async function detectManifestActions( count: 0, conda: false, gradle: false, + maven: false, sbt: false, } @@ -76,6 +78,17 @@ export async function detectManifestActions( output.count += 1 } + if (sockJson?.defaults?.manifest?.maven?.disabled) { + debugLog( + 'notice', + `[DEBUG] - maven auto-detection is disabled in ${SOCKET_JSON}`, + ) + } else if (existsSync(path.join(cwd, 'pom.xml'))) { + debugLog('notice', '[DEBUG] - Detected a Maven pom.xml build file') + output.maven = true + output.count += 1 + } + if (sockJson?.defaults?.manifest?.conda?.disabled) { debugLog( 'notice', diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index 0794be55b..7e34f78a4 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -4,6 +4,7 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { extractBazelToMaven } from './bazel/extract_bazel_to_maven.mts' import { convertGradleToFacts } from './convert-gradle-to-facts.mts' +import { convertMavenToFacts } from './convert-maven-to-facts.mts' import { convertSbtToFacts } from './convert-sbt-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' import { convertSbtToMaven } from './convert_sbt_to_maven.mts' @@ -112,6 +113,26 @@ export async function generateAutoManifest({ } } + if (!sockJson?.defaults?.manifest?.maven?.disabled && detected.maven) { + logger.log('Detected a Maven pom.xml build, generating Socket facts...') + await convertMavenToFacts({ + // Note: `mvn` is more likely to be resolved against PATH env. + bin: sockJson.defaults?.manifest?.maven?.bin ?? 'mvn', + cwd, + excludeConfigs: sockJson.defaults?.manifest?.maven?.excludeConfigs ?? '', + ignoreUnresolved: Boolean( + sockJson.defaults?.manifest?.maven?.ignoreUnresolved, + ), + includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '', + mavenOpts: + sockJson.defaults?.manifest?.maven?.mavenOpts + ?.split(' ') + .map(s => s.trim()) + .filter(Boolean) ?? [], + verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose), + }) + } + if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) { logger.log( 'Detected an environment.yml file, running default Conda generator...', diff --git a/src/commands/manifest/setup-manifest-config.mts b/src/commands/manifest/setup-manifest-config.mts index c6cb97b0a..1e13a0f90 100644 --- a/src/commands/manifest/setup-manifest-config.mts +++ b/src/commands/manifest/setup-manifest-config.mts @@ -69,6 +69,11 @@ export async function setupManifestConfig( description: 'Generate a Socket facts file or pom.xml (for Kotlin) through gradle', }, + { + name: 'Maven'.padEnd(30, ' '), + value: 'maven', + description: 'Generate a Socket facts file through maven', + }, { name: 'Scala (gradle)'.padEnd(30, ' '), value: 'gradle', @@ -147,6 +152,13 @@ export async function setupManifestConfig( result = await setupGradle(sockJson.defaults.manifest.gradle) break } + case 'maven': { + if (!sockJson.defaults.manifest.maven) { + sockJson.defaults.manifest.maven = {} + } + result = await setupMaven(sockJson.defaults.manifest.maven) + break + } case 'sbt': { if (!sockJson.defaults.manifest.sbt) { sockJson.defaults.manifest.sbt = {} @@ -315,6 +327,52 @@ async function setupGradle( return notCanceled() } +async function setupMaven( + config: NonNullable< + NonNullable['manifest']>['maven'] + >, +): Promise> { + const bin = await askForBin(config.bin || 'mvn') + if (bin === undefined) { + return canceledByUser() + } else if (bin) { + config.bin = bin + } else { + delete config.bin + } + + const opts = await input({ + message: '(--maven-opts) Enter maven options to pass through', + default: config.mavenOpts || '', + required: false, + }) + if (opts === undefined) { + return canceledByUser() + } else if (opts) { + config.mavenOpts = opts + } else { + delete config.mavenOpts + } + + // Maven only generates Socket facts (no pom path), so always ask the + // facts-only options. + const factsOptions = await setupFactsOptions(config) + if (!factsOptions.ok || factsOptions.data.canceled) { + return factsOptions + } + + const verbose = await askForVerboseFlag(config.verbose) + if (verbose === undefined) { + return canceledByUser() + } else if (verbose === 'yes' || verbose === 'no') { + config.verbose = verbose === 'yes' + } else { + delete config.verbose + } + + return notCanceled() +} + async function setupSbt( config: NonNullable< NonNullable['manifest']>['sbt'] diff --git a/src/utils/socket-json.mts b/src/utils/socket-json.mts index e8c7ad257..3cbbfee23 100644 --- a/src/utils/socket-json.mts +++ b/src/utils/socket-json.mts @@ -69,6 +69,15 @@ export interface SocketJson { ignoreUnresolved?: boolean | undefined verbose?: boolean | undefined } + maven?: { + disabled?: boolean | undefined + bin?: string | undefined + excludeConfigs?: string | undefined + includeConfigs?: string | undefined + ignoreUnresolved?: boolean | undefined + mavenOpts?: string | undefined + verbose?: boolean | undefined + } sbt?: { disabled?: boolean | undefined infile?: string | undefined From aee350906403f9fb9de5a84bb7f71cb64f42b2cf Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Mon, 22 Jun 2026 10:25:17 +0200 Subject: [PATCH 2/3] fix(manifest): correct 1.1.125 changelog label; quote-aware build-tool opts Address review feedback on the maven PR: - CHANGELOG: the new section is the 1.1.125 release (per package.json after the v1.x merge), so relabel its heading `1.1.124` -> `1.1.125` (the link target was already v1.1.125). - `--gradle-opts` / `--sbt-opts` / `--maven-opts` were split on every space, shredding a value with a spaced path (e.g. `-s "my settings.xml"`) into separate tokens. Introduce a shared quote-aware tokenizer (`parseBuildToolOpts`) honoring single/double quotes and use it across all manifest opts sites (gradle/kotlin/scala/maven + auto-manifest) so the fix is consistent rather than a maven-only divergence. Unquoted input tokenizes exactly as before. --- CHANGELOG.md | 2 +- src/commands/manifest/cmd-manifest-gradle.mts | 6 +-- src/commands/manifest/cmd-manifest-kotlin.mts | 6 +-- src/commands/manifest/cmd-manifest-maven.mts | 6 +-- src/commands/manifest/cmd-manifest-scala.mts | 6 +-- .../manifest/generate_auto_manifest.mts | 23 +++----- .../manifest/parse-build-tool-opts.mts | 52 +++++++++++++++++++ .../manifest/parse-build-tool-opts.test.mts | 42 +++++++++++++++ 8 files changed, 111 insertions(+), 32 deletions(-) create mode 100644 src/commands/manifest/parse-build-tool-opts.mts create mode 100644 src/commands/manifest/parse-build-tool-opts.test.mts diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b8ecef0..6a4732180 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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/). -## [1.1.124](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.125) - 2026-06-19 +## [1.1.125](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.125) - 2026-06-19 ### Added - New `socket manifest maven` command generates a Socket facts file (`.socket.facts.json`) directly from a Maven `pom.xml` project. Like the Gradle and sbt generators, it auto-detects your project, plugs into `socket manifest auto` and the `socket manifest setup` configurator, and accepts `--maven-opts` to pass options through to Maven (e.g. `--maven-opts="-P release -s settings.xml"`), plus `--bin` to point at a wrapper such as `./mvnw`. diff --git a/src/commands/manifest/cmd-manifest-gradle.mts b/src/commands/manifest/cmd-manifest-gradle.mts index 438200ae9..5f8b1d43b 100644 --- a/src/commands/manifest/cmd-manifest-gradle.mts +++ b/src/commands/manifest/cmd-manifest-gradle.mts @@ -5,6 +5,7 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { convertGradleToFacts } from './convert-gradle-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' +import { parseBuildToolOpts } from './parse-build-tool-opts.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' @@ -280,10 +281,7 @@ async function run( return } - const parsedGradleOpts = String(gradleOpts || '') - .split(' ') - .map(s => s.trim()) - .filter(Boolean) + const parsedGradleOpts = parseBuildToolOpts(String(gradleOpts || '')) if (facts) { await convertGradleToFacts({ diff --git a/src/commands/manifest/cmd-manifest-kotlin.mts b/src/commands/manifest/cmd-manifest-kotlin.mts index d22af4b34..1bcc008ac 100644 --- a/src/commands/manifest/cmd-manifest-kotlin.mts +++ b/src/commands/manifest/cmd-manifest-kotlin.mts @@ -5,6 +5,7 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { convertGradleToFacts } from './convert-gradle-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' +import { parseBuildToolOpts } from './parse-build-tool-opts.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' @@ -283,10 +284,7 @@ async function run( return } - const parsedGradleOpts = String(gradleOpts || '') - .split(' ') - .map(s => s.trim()) - .filter(Boolean) + const parsedGradleOpts = parseBuildToolOpts(String(gradleOpts || '')) if (facts) { await convertGradleToFacts({ diff --git a/src/commands/manifest/cmd-manifest-maven.mts b/src/commands/manifest/cmd-manifest-maven.mts index 7db110028..b2b863e3c 100644 --- a/src/commands/manifest/cmd-manifest-maven.mts +++ b/src/commands/manifest/cmd-manifest-maven.mts @@ -4,6 +4,7 @@ import { debugFn } from '@socketsecurity/registry/lib/debug' import { logger } from '@socketsecurity/registry/lib/logger' import { convertMavenToFacts } from './convert-maven-to-facts.mts' +import { parseBuildToolOpts } from './parse-build-tool-opts.mts' import constants, { SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' @@ -218,10 +219,7 @@ async function run( return } - const parsedMavenOpts = String(mavenOpts || '') - .split(' ') - .map(s => s.trim()) - .filter(Boolean) + const parsedMavenOpts = parseBuildToolOpts(String(mavenOpts || '')) await convertMavenToFacts({ bin: String(bin), diff --git a/src/commands/manifest/cmd-manifest-scala.mts b/src/commands/manifest/cmd-manifest-scala.mts index 011123056..6d4861b7e 100644 --- a/src/commands/manifest/cmd-manifest-scala.mts +++ b/src/commands/manifest/cmd-manifest-scala.mts @@ -5,6 +5,7 @@ import { logger } from '@socketsecurity/registry/lib/logger' import { convertSbtToFacts } from './convert-sbt-to-facts.mts' import { convertSbtToMaven } from './convert_sbt_to_maven.mts' +import { parseBuildToolOpts } from './parse-build-tool-opts.mts' import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' @@ -337,10 +338,7 @@ async function run( return } - const parsedSbtOpts = String(sbtOpts || '') - .split(' ') - .map(s => s.trim()) - .filter(Boolean) + const parsedSbtOpts = parseBuildToolOpts(String(sbtOpts || '')) if (facts) { await convertSbtToFacts({ diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index 7e34f78a4..1742d2d89 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -9,6 +9,7 @@ import { convertSbtToFacts } from './convert-sbt-to-facts.mts' import { convertGradleToMaven } from './convert_gradle_to_maven.mts' import { convertSbtToMaven } from './convert_sbt_to_maven.mts' import { handleManifestConda } from './handle-manifest-conda.mts' +import { parseBuildToolOpts } from './parse-build-tool-opts.mts' import { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' @@ -45,11 +46,7 @@ export async function generateAutoManifest({ // Note: `sbt` is more likely to be resolved against PATH env. bin: sockJson.defaults?.manifest?.sbt?.bin ?? 'sbt', cwd, - sbtOpts: - sockJson.defaults?.manifest?.sbt?.sbtOpts - ?.split(' ') - .map(s => s.trim()) - .filter(Boolean) ?? [], + sbtOpts: parseBuildToolOpts(sockJson.defaults?.manifest?.sbt?.sbtOpts), verbose: Boolean(sockJson.defaults?.manifest?.sbt?.verbose), } // Socket facts is the default; opt into pom generation with @@ -83,11 +80,9 @@ export async function generateAutoManifest({ : path.join(cwd, 'gradlew'), cwd, verbose: Boolean(sockJson.defaults?.manifest?.gradle?.verbose), - gradleOpts: - sockJson.defaults?.manifest?.gradle?.gradleOpts - ?.split(' ') - .map(s => s.trim()) - .filter(Boolean) ?? [], + gradleOpts: parseBuildToolOpts( + sockJson.defaults?.manifest?.gradle?.gradleOpts, + ), } // Socket facts is the default; opt into pom generation with // `defaults.manifest.gradle.facts: false` in socket.json. @@ -124,11 +119,9 @@ export async function generateAutoManifest({ sockJson.defaults?.manifest?.maven?.ignoreUnresolved, ), includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '', - mavenOpts: - sockJson.defaults?.manifest?.maven?.mavenOpts - ?.split(' ') - .map(s => s.trim()) - .filter(Boolean) ?? [], + mavenOpts: parseBuildToolOpts( + sockJson.defaults?.manifest?.maven?.mavenOpts, + ), verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose), }) } diff --git a/src/commands/manifest/parse-build-tool-opts.mts b/src/commands/manifest/parse-build-tool-opts.mts new file mode 100644 index 000000000..b7018eafe --- /dev/null +++ b/src/commands/manifest/parse-build-tool-opts.mts @@ -0,0 +1,52 @@ +// Tokenizes a build-tool options string (e.g. the value of `--gradle-opts`, +// `--sbt-opts`, `--maven-opts`) into individual argv tokens. Splits on +// whitespace but honors single and double quotes so a value containing spaces, +// such as a settings path (`-s "my settings.xml"`), survives as one token +// instead of being shredded into three. Quotes are consumed (not emitted), and +// quoting is intra-token aware (`-Dkey="a b"` -> `-Dkey=a b`). For unquoted +// input this is equivalent to the previous whitespace split. +export function parseBuildToolOpts(opts: string | undefined): string[] { + if (!opts) { + return [] + } + const tokens: string[] = [] + let current = '' + let hasToken = false + let inSingle = false + let inDouble = false + for (let i = 0; i < opts.length; i += 1) { + const ch = opts[i] + if (inSingle) { + if (ch === "'") { + inSingle = false + } else { + current += ch + } + } else if (inDouble) { + if (ch === '"') { + inDouble = false + } else { + current += ch + } + } else if (ch === "'") { + inSingle = true + hasToken = true + } else if (ch === '"') { + inDouble = true + hasToken = true + } else if (ch === ' ' || ch === '\t') { + if (hasToken) { + tokens.push(current) + current = '' + hasToken = false + } + } else { + current += ch + hasToken = true + } + } + if (hasToken) { + tokens.push(current) + } + return tokens +} diff --git a/src/commands/manifest/parse-build-tool-opts.test.mts b/src/commands/manifest/parse-build-tool-opts.test.mts new file mode 100644 index 000000000..50111c44e --- /dev/null +++ b/src/commands/manifest/parse-build-tool-opts.test.mts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest' + +import { parseBuildToolOpts } from './parse-build-tool-opts.mts' + +describe('parseBuildToolOpts', () => { + it('returns an empty array for empty/undefined input', () => { + expect(parseBuildToolOpts(undefined)).toEqual([]) + expect(parseBuildToolOpts('')).toEqual([]) + expect(parseBuildToolOpts(' ')).toEqual([]) + }) + + it('splits a plain space-separated string (parity with the old split)', () => { + expect(parseBuildToolOpts('-P release -s settings.xml')).toEqual([ + '-P', + 'release', + '-s', + 'settings.xml', + ]) + }) + + it('collapses runs of whitespace and trims', () => { + expect(parseBuildToolOpts(' -P release ')).toEqual(['-P', 'release']) + }) + + it('keeps a double-quoted value with spaces as one token', () => { + expect(parseBuildToolOpts('-s "my settings.xml"')).toEqual([ + '-s', + 'my settings.xml', + ]) + }) + + it('keeps a single-quoted value with spaces as one token', () => { + expect(parseBuildToolOpts("-s 'my settings.xml'")).toEqual([ + '-s', + 'my settings.xml', + ]) + }) + + it('handles quotes inside a token', () => { + expect(parseBuildToolOpts('-Dprop="a b"')).toEqual(['-Dprop=a b']) + }) +}) From b0c243b19ed041a8de852b373c9af94877c470d9 Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Mon, 22 Jun 2026 10:29:33 +0200 Subject: [PATCH 3/3] fix changelog date --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a4732180..a870e606c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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/). -## [1.1.125](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.125) - 2026-06-19 +## [1.1.125](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.125) - 2026-06-22 ### Added - New `socket manifest maven` command generates a Socket facts file (`.socket.facts.json`) directly from a Maven `pom.xml` project. Like the Gradle and sbt generators, it auto-detects your project, plugs into `socket manifest auto` and the `socket manifest setup` configurator, and accepts `--maven-opts` to pass options through to Maven (e.g. `--maven-opts="-P release -s settings.xml"`), plus `--bin` to point at a wrapper such as `./mvnw`.