From 18f7e65b6b1e93134175421678d1090208b0985b Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Mon, 22 Jun 2026 10:50:58 +0200 Subject: [PATCH] feat(manifest): add `socket manifest dotnet` (1.1.126, Coana 15.5.6) Add a `socket manifest dotnet` command that generates a Socket facts file (`.socket.facts.json`) from a .NET project by delegating to the Coana CLI's `manifest dotnet` command (which runs a bundled NuGet/MSBuild resolver for SDK-style and legacy `packages.config` projects), mirroring the existing gradle/sbt/maven facts flows. Includes detection of `*.csproj`/`*.fsproj`/ `*.vbproj`/`*.sln` project/solution files, `socket manifest auto` wiring, the `socket manifest setup` configurator, socket.json defaults, and `--dotnet-opts` / `--bin` pass-through. Unlike the JVM tools, the .NET resolver has no configuration filters, so `--include-configs`/`--exclude-configs` are intentionally not exposed; only `--ignore-unresolved` and `--dotnet-opts` apply. Bump Coana CLI to 15.5.6, which adds the `manifest dotnet` command this delegates to. --- CHANGELOG.md | 8 + package.json | 4 +- pnpm-lock.yaml | 10 +- src/commands/manifest/README.md | 10 + src/commands/manifest/cmd-manifest-dotnet.mts | 195 ++++++++++++++++++ .../manifest/cmd-manifest-dotnet.test.mts | 82 ++++++++ src/commands/manifest/cmd-manifest.mts | 6 +- src/commands/manifest/cmd-manifest.test.mts | 1 + .../manifest/coana-manifest-facts.mts | 17 +- .../manifest/convert-dotnet-to-facts.mts | 36 ++++ .../manifest/detect-manifest-actions.mts | 37 +++- .../manifest/generate_auto_manifest.mts | 17 ++ .../manifest/setup-manifest-config.mts | 63 ++++++ src/utils/socket-json.mts | 7 + 14 files changed, 477 insertions(+), 16 deletions(-) create mode 100644 src/commands/manifest/cmd-manifest-dotnet.mts create mode 100644 src/commands/manifest/cmd-manifest-dotnet.test.mts create mode 100644 src/commands/manifest/convert-dotnet-to-facts.mts diff --git a/CHANGELOG.md b/CHANGELOG.md index a870e606c..d5f29f3fa 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.126](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.126) - 2026-06-22 + +### Added +- New `socket manifest dotnet` command generates a Socket facts file (`.socket.facts.json`) from a .NET project (`.csproj`/`.fsproj`/`.vbproj`/`.sln`). It runs the `dotnet` host on PATH to resolve NuGet/MSBuild dependencies (SDK-style and legacy `packages.config` projects), auto-detects your project, and plugs into `socket manifest auto` and the `socket manifest setup` configurator. Use `--bin` to point at a specific dotnet host and `--dotnet-opts` to pass options through. + +### Changed +- Updated the Coana CLI to v `15.5.6`. + ## [1.1.125](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.125) - 2026-06-22 ### Added diff --git a/package.json b/package.json index 650cc06b4..628aaa610 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.125", + "version": "1.1.126", "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.5", + "@coana-tech/cli": "15.5.6", "@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 d588dc211..16d8efae8 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.5 - version: 15.5.5 + specifier: 15.5.6 + version: 15.5.6 '@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.5': - resolution: {integrity: sha512-eFZ1q1i7Xr8gEA80OfmzIoQXkDjrF3AUeRYaOvibVW6bJMJfDTBmxRkYtks6sloGgbNR9X/8Lwy4V+Z+D6llpA==} + '@coana-tech/cli@15.5.6': + resolution: {integrity: sha512-knPb8U08H93Q9hFZjpGK+tdD9mlVidnzmjewkloiuxY1KWa1T5mxB7CSZCk1XeNAqoACCjmGeOZhvX80r3HILw==} 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.5': {} + '@coana-tech/cli@15.5.6': {} '@colors/colors@1.5.0': optional: true diff --git a/src/commands/manifest/README.md b/src/commands/manifest/README.md index 0f54b10a7..26c8f8f03 100644 --- a/src/commands/manifest/README.md +++ b/src/commands/manifest/README.md @@ -140,6 +140,16 @@ have a working cdxgen configuration. Converts a Conda `environment.yml` file to a Python `requirements.txt` so the Socket scan pipeline can consume the resulting manifest. +## socket manifest dotnet [beta] + +Generates a Socket facts file (`.socket.facts.json`) from a .NET project +(`*.csproj` / `*.fsproj` / `*.vbproj` / `*.sln`), using the `dotnet` host on +PATH to run a bundled NuGet/MSBuild resolver (SDK-style and legacy +`packages.config` projects are both supported). Override the host with `--bin` +and pass extra options through with `--dotnet-opts`. Unlike the JVM generators +there are no configuration filters (`--include-configs` / `--exclude-configs` +do not apply). + ## socket manifest gradle [beta] Uses Gradle (via the project's `gradlew`) to emit a `pom.xml` per subproject, diff --git a/src/commands/manifest/cmd-manifest-dotnet.mts b/src/commands/manifest/cmd-manifest-dotnet.mts new file mode 100644 index 000000000..2be5b7ee6 --- /dev/null +++ b/src/commands/manifest/cmd-manifest-dotnet.mts @@ -0,0 +1,195 @@ +import path from 'node:path' + +import { debugFn } from '@socketsecurity/registry/lib/debug' +import { logger } from '@socketsecurity/registry/lib/logger' + +import { convertDotnetToFacts } from './convert-dotnet-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' +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: 'dotnet', + description: + '[beta] Generate a Socket facts file from a .NET project (`.csproj`/`.sln`/etc)', + hidden: false, + flags: { + ...commonFlags, + bin: { + type: 'string', + description: + 'Location of the dotnet host to use, default: dotnet on PATH', + }, + ignoreUnresolved: { + type: 'boolean', + description: + 'Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)', + }, + dotnetOpts: { + type: 'string', + description: 'Additional options to pass on to the bundled dotnet tool', + }, + 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 .NET project, using the \`dotnet\` host on PATH to run a + bundled NuGet/MSBuild resolver (SDK-style projects and legacy + \`packages.config\` are both supported). An unresolved dependency is a fatal + error; pass --ignore-unresolved to warn and continue instead. + + Unlike the JVM generators there are no configuration filters: .NET + resolution has no equivalent of Gradle/Maven configurations, so + --include-configs / --exclude-configs do not apply. + + You can specify --bin to override the path to the \`dotnet\` host to invoke, + and --dotnet-opts to pass extra options through to the bundled tool. + + Support is beta. Please report issues or give us feedback on what's missing. + + Examples + + $ ${command} . + $ ${command} --bin=/usr/local/share/dotnet/dotnet . + `, +} + +export const cmdManifestDotnet = { + 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} dotnet`, + sockJson?.defaults?.manifest?.dotnet, + ) + + let { bin, dotnetOpts, ignoreUnresolved, verbose } = cli.flags + + // Set defaults for any flag/arg that is not given. Check socket.json first. + if (!bin) { + if (sockJson.defaults?.manifest?.dotnet?.bin) { + bin = sockJson.defaults?.manifest?.dotnet?.bin + logger.info(`Using default --bin from ${SOCKET_JSON}:`, bin) + } else { + bin = 'dotnet' + } + } + if (!dotnetOpts) { + if (sockJson.defaults?.manifest?.dotnet?.dotnetOpts) { + dotnetOpts = sockJson.defaults?.manifest?.dotnet?.dotnetOpts + logger.info( + `Using default --dotnet-opts from ${SOCKET_JSON}:`, + dotnetOpts, + ) + } else { + dotnetOpts = '' + } + } + if (ignoreUnresolved === undefined) { + if (sockJson.defaults?.manifest?.dotnet?.ignoreUnresolved !== undefined) { + ignoreUnresolved = sockJson.defaults?.manifest?.dotnet?.ignoreUnresolved + logger.info( + `Using default --ignore-unresolved from ${SOCKET_JSON}:`, + ignoreUnresolved, + ) + } else { + ignoreUnresolved = false + } + } + if (verbose === undefined) { + if (sockJson.defaults?.manifest?.dotnet?.verbose !== undefined) { + verbose = sockJson.defaults?.manifest?.dotnet?.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('- dotnet bin:', bin) + logger.groupEnd() + } + + if (dryRun) { + logger.log(constants.DRY_RUN_BAILING_NOW) + return + } + + const parsedDotnetOpts = parseBuildToolOpts(String(dotnetOpts || '')) + + await convertDotnetToFacts({ + bin: String(bin), + cwd, + dotnetOpts: parsedDotnetOpts, + ignoreUnresolved: Boolean(ignoreUnresolved), + verbose: Boolean(verbose), + }) +} diff --git a/src/commands/manifest/cmd-manifest-dotnet.test.mts b/src/commands/manifest/cmd-manifest-dotnet.test.mts new file mode 100644 index 000000000..32412ee27 --- /dev/null +++ b/src/commands/manifest/cmd-manifest-dotnet.test.mts @@ -0,0 +1,82 @@ +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 dotnet', async () => { + const { binCliPath } = constants + + cmdit( + ['manifest', 'dotnet', 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 .NET project (\`.csproj\`/\`.sln\`/etc) + + Usage + $ socket manifest dotnet [options] [CWD=.] + + Options + --bin Location of the dotnet host to use, default: dotnet on PATH + --dotnet-opts Additional options to pass on to the bundled dotnet tool + --ignore-unresolved Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file) + --verbose Print debug messages + + Emits a single \`.socket.facts.json\` describing the resolved dependency + graph of your .NET project, using the \`dotnet\` host on PATH to run a + bundled NuGet/MSBuild resolver (SDK-style projects and legacy + \`packages.config\` are both supported). An unresolved dependency is a fatal + error; pass --ignore-unresolved to warn and continue instead. + + Unlike the JVM generators there are no configuration filters: .NET + resolution has no equivalent of Gradle/Maven configurations, so + --include-configs / --exclude-configs do not apply. + + You can specify --bin to override the path to the \`dotnet\` host to invoke, + and --dotnet-opts to pass extra options through to the bundled tool. + + Support is beta. Please report issues or give us feedback on what's missing. + + Examples + + $ socket manifest dotnet . + $ socket manifest dotnet --bin=/usr/local/share/dotnet/dotnet ." + `) + expect(`\n ${stderr}`).toMatchInlineSnapshot(` + " + _____ _ _ /--------------- + | __|___ ___| |_ ___| |_ | CLI: + |__ | * | _| '_| -_| _| | token: , org: + |_____|___|___|_,_|___|_|.dev | Command: \`socket manifest dotnet\`, cwd: " + `) + + expect(code, 'explicit help should exit with code 0').toBe(0) + expect(stderr, 'banner includes base command').toContain( + '`socket manifest dotnet`', + ) + }, + ) + + cmdit( + ['manifest', 'dotnet', 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 dotnet\`, 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 518cdfddd..dcc0ae986 100644 --- a/src/commands/manifest/cmd-manifest.mts +++ b/src/commands/manifest/cmd-manifest.mts @@ -2,6 +2,7 @@ import { cmdManifestBazel } from './bazel/cmd-manifest-bazel.mts' import { cmdManifestAuto } from './cmd-manifest-auto.mts' import { cmdManifestCdxgen } from './cmd-manifest-cdxgen.mts' import { cmdManifestConda } from './cmd-manifest-conda.mts' +import { cmdManifestDotnet } from './cmd-manifest-dotnet.mts' import { cmdManifestGradle } from './cmd-manifest-gradle.mts' import { cmdManifestKotlin } from './cmd-manifest-kotlin.mts' import { cmdManifestMaven } from './cmd-manifest-maven.mts' @@ -39,8 +40,8 @@ const config: CliCommandConfig = { configurations available. See \`manifest --help\` for usage details per language. - Currently supported language: bazel [beta], gradle [beta], kotlin (through - gradle) [beta], maven [beta], scala [beta]. + Currently supported language: bazel [beta], dotnet [beta], gradle [beta], + kotlin (through gradle) [beta], maven [beta], scala [beta]. Examples @@ -73,6 +74,7 @@ async function run( bazel: cmdManifestBazel, cdxgen: cmdManifestCdxgen, conda: cmdManifestConda, + dotnet: cmdManifestDotnet, gradle: cmdManifestGradle, kotlin: cmdManifestKotlin, maven: cmdManifestMaven, diff --git a/src/commands/manifest/cmd-manifest.test.mts b/src/commands/manifest/cmd-manifest.test.mts index 93c264770..7c6b0d27b 100644 --- a/src/commands/manifest/cmd-manifest.test.mts +++ b/src/commands/manifest/cmd-manifest.test.mts @@ -27,6 +27,7 @@ describe('socket manifest', async () => { bazel [beta] Bazel SBOM support \\u2014 generate manifest files for a Bazel project (Maven, PyPI) cdxgen Run cdxgen for SBOM generation conda [beta] Convert a Conda environment.yml file to a python requirements.txt + dotnet [beta] Generate a Socket facts file from a .NET project (\`.csproj\`/\`.sln\`/etc) 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 diff --git a/src/commands/manifest/coana-manifest-facts.mts b/src/commands/manifest/coana-manifest-facts.mts index 47ac6fb72..95e2ba73b 100644 --- a/src/commands/manifest/coana-manifest-facts.mts +++ b/src/commands/manifest/coana-manifest-facts.mts @@ -17,10 +17,11 @@ 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/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 ''`. +// `SOCKET_CLI_COANA_LOCAL_PATH` is set). `bin` (the build-tool executable) is +// always resolved by the caller to a concrete default (`/gradlew`, or +// `mvn`/`sbt`/`dotnet` 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 +35,13 @@ export async function runCoanaManifestFacts({ }: { bin: string buildOpts: string[] - buildOptsFlag: '--gradle-opts' | '--maven-opts' | '--sbt-opts' + buildOptsFlag: + | '--dotnet-opts' + | '--gradle-opts' + | '--maven-opts' + | '--sbt-opts' cwd: string - ecosystem: 'gradle' | 'maven' | 'sbt' + ecosystem: 'dotnet' | 'gradle' | 'maven' | 'sbt' excludeConfigs: string ignoreUnresolved: boolean includeConfigs: string diff --git a/src/commands/manifest/convert-dotnet-to-facts.mts b/src/commands/manifest/convert-dotnet-to-facts.mts new file mode 100644 index 000000000..5834852cf --- /dev/null +++ b/src/commands/manifest/convert-dotnet-to-facts.mts @@ -0,0 +1,36 @@ +import { runCoanaManifestFacts } from './coana-manifest-facts.mts' + +// Generates a `.socket.facts.json` for a .NET project by delegating to the Coana +// CLI's `manifest dotnet` command (which runs a bundled .NET tool that performs +// native NuGet/MSBuild resolution). socket-cli no longer runs dotnet itself; an +// explicit `bin` is forwarded as `--bin` (the dotnet host), otherwise Coana +// defaults to `dotnet` on PATH. +// +// Unlike the JVM tools, the dotnet resolver has no notion of configuration +// filters, so `--include-configs` / `--exclude-configs` are intentionally not +// exposed; only `--ignore-unresolved` and `--dotnet-opts` apply. +export async function convertDotnetToFacts({ + bin, + cwd, + dotnetOpts, + ignoreUnresolved, + verbose, +}: { + bin: string + cwd: string + dotnetOpts: string[] + ignoreUnresolved: boolean + verbose: boolean +}): Promise { + await runCoanaManifestFacts({ + bin, + buildOpts: dotnetOpts, + buildOptsFlag: '--dotnet-opts', + cwd, + ecosystem: 'dotnet', + excludeConfigs: '', + ignoreUnresolved, + includeConfigs: '', + verbose, + }) +} diff --git a/src/commands/manifest/detect-manifest-actions.mts b/src/commands/manifest/detect-manifest-actions.mts index 7be3f7330..9747a5c1c 100644 --- a/src/commands/manifest/detect-manifest-actions.mts +++ b/src/commands/manifest/detect-manifest-actions.mts @@ -1,7 +1,7 @@ // The point here is to attempt to detect the various supported manifest files // the CLI can generate. This would be environments that we can't do server side -import { existsSync } from 'node:fs' +import { existsSync, readdirSync } from 'node:fs' import path from 'node:path' import { debugLog } from '@socketsecurity/registry/lib/debug' @@ -19,11 +19,34 @@ export interface GeneratableManifests { cdxgen: boolean count: number conda: boolean + dotnet: boolean gradle: boolean maven: boolean sbt: boolean } +// .NET has no single fixed manifest filename; a project or solution file +// (`*.sln`, `*.csproj`, `*.fsproj`, `*.vbproj`) at the directory root is the +// trigger. Coana's bundled tool handles discovery/restore from there. +const DOTNET_PROJECT_EXTENSIONS = new Set([ + '.csproj', + '.fsproj', + '.sln', + '.vbproj', +]) + +function hasDotnetProjectFile(cwd: string): boolean { + let entries: string[] + try { + entries = readdirSync(cwd) + } catch { + return false + } + return entries.some(name => + DOTNET_PROJECT_EXTENSIONS.has(path.extname(name).toLowerCase()), + ) +} + export async function detectManifestActions( // Passing in null means we attempt detection for every supported language // regardless of local socket.json status. Sometimes we want that. @@ -35,6 +58,7 @@ export async function detectManifestActions( cdxgen: false, // TODO count: 0, conda: false, + dotnet: false, gradle: false, maven: false, sbt: false, @@ -89,6 +113,17 @@ export async function detectManifestActions( output.count += 1 } + if (sockJson?.defaults?.manifest?.dotnet?.disabled) { + debugLog( + 'notice', + `[DEBUG] - dotnet auto-detection is disabled in ${SOCKET_JSON}`, + ) + } else if (hasDotnetProjectFile(cwd)) { + debugLog('notice', '[DEBUG] - Detected a .NET project/solution file') + output.dotnet = 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 1742d2d89..5a7a3fdde 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -3,6 +3,7 @@ import path from 'node:path' import { logger } from '@socketsecurity/registry/lib/logger' import { extractBazelToMaven } from './bazel/extract_bazel_to_maven.mts' +import { convertDotnetToFacts } from './convert-dotnet-to-facts.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' @@ -126,6 +127,22 @@ export async function generateAutoManifest({ }) } + if (!sockJson?.defaults?.manifest?.dotnet?.disabled && detected.dotnet) { + logger.log('Detected a .NET project, generating Socket facts...') + await convertDotnetToFacts({ + // Note: `dotnet` is more likely to be resolved against PATH env. + bin: sockJson.defaults?.manifest?.dotnet?.bin ?? 'dotnet', + cwd, + dotnetOpts: parseBuildToolOpts( + sockJson.defaults?.manifest?.dotnet?.dotnetOpts, + ), + ignoreUnresolved: Boolean( + sockJson.defaults?.manifest?.dotnet?.ignoreUnresolved, + ), + verbose: Boolean(sockJson.defaults?.manifest?.dotnet?.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 1e13a0f90..54a3d3939 100644 --- a/src/commands/manifest/setup-manifest-config.mts +++ b/src/commands/manifest/setup-manifest-config.mts @@ -58,6 +58,11 @@ export async function setupManifestConfig( value: 'conda', description: `Generate ${REQUIREMENTS_TXT} from a Conda environment.yml`, }, + { + name: '.NET'.padEnd(30, ' '), + value: 'dotnet', + description: 'Generate a Socket facts file through dotnet', + }, { name: 'Gradle'.padEnd(30, ' '), value: 'gradle', @@ -145,6 +150,13 @@ export async function setupManifestConfig( result = await setupConda(sockJson.defaults.manifest.conda) break } + case 'dotnet': { + if (!sockJson.defaults.manifest.dotnet) { + sockJson.defaults.manifest.dotnet = {} + } + result = await setupDotnet(sockJson.defaults.manifest.dotnet) + break + } case 'gradle': { if (!sockJson.defaults.manifest.gradle) { sockJson.defaults.manifest.gradle = {} @@ -269,6 +281,57 @@ async function setupConda( return notCanceled() } +async function setupDotnet( + config: NonNullable< + NonNullable['manifest']>['dotnet'] + >, +): Promise> { + const bin = await askForBin(config.bin || 'dotnet') + if (bin === undefined) { + return canceledByUser() + } else if (bin) { + config.bin = bin + } else { + delete config.bin + } + + const opts = await input({ + message: '(--dotnet-opts) Enter dotnet options to pass through', + default: config.dotnetOpts || '', + required: false, + }) + if (opts === undefined) { + return canceledByUser() + } else if (opts) { + config.dotnetOpts = opts + } else { + delete config.dotnetOpts + } + + // .NET resolution has no config filters; only --ignore-unresolved applies. + const ignoreUnresolved = await askForIgnoreUnresolvedFlag( + config.ignoreUnresolved, + ) + if (ignoreUnresolved === undefined) { + return canceledByUser() + } else if (ignoreUnresolved === 'yes' || ignoreUnresolved === 'no') { + config.ignoreUnresolved = ignoreUnresolved === 'yes' + } else { + delete config.ignoreUnresolved + } + + 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 setupGradle( config: NonNullable< NonNullable['manifest']>['gradle'] diff --git a/src/utils/socket-json.mts b/src/utils/socket-json.mts index 3cbbfee23..2a93049e3 100644 --- a/src/utils/socket-json.mts +++ b/src/utils/socket-json.mts @@ -59,6 +59,13 @@ export interface SocketJson { target?: string | undefined verbose?: boolean | undefined } + dotnet?: { + disabled?: boolean | undefined + bin?: string | undefined + dotnetOpts?: string | undefined + ignoreUnresolved?: boolean | undefined + verbose?: boolean | undefined + } gradle?: { disabled?: boolean | undefined bin?: string | undefined