|
| 1 | +import { statSync } from 'node:fs' |
| 2 | +import path from 'node:path' |
| 3 | + |
| 4 | +import { debugFn } from '@socketsecurity/registry/lib/debug' |
| 5 | +import { logger } from '@socketsecurity/registry/lib/logger' |
| 6 | + |
| 7 | +import { convertDotnetToFacts } from './convert-dotnet-to-facts.mts' |
| 8 | +import { excludePathsFlag } from './manifest-flags.mts' |
| 9 | +import { parseBuildToolOpts } from './parse-build-tool-opts.mts' |
| 10 | +import { resolveBuildToolBin } from './scripts/build-tool.mts' |
| 11 | +import constants, { SOCKET_JSON } from '../../constants.mts' |
| 12 | +import { commonFlags } from '../../flags.mts' |
| 13 | +import { checkCommandInput } from '../../utils/check-input.mts' |
| 14 | +import { cmdFlagValueToArray } from '../../utils/cmd.mts' |
| 15 | +import { getOutputKind } from '../../utils/get-output-kind.mts' |
| 16 | +import { meowOrExit } from '../../utils/meow-with-subcommands.mts' |
| 17 | +import { getFlagListOutput } from '../../utils/output-formatting.mts' |
| 18 | +import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' |
| 19 | +import { assertValidExcludePaths } from '../scan/exclude-paths.mts' |
| 20 | + |
| 21 | +import type { |
| 22 | + CliCommandConfig, |
| 23 | + CliCommandContext, |
| 24 | +} from '../../utils/meow-with-subcommands.mts' |
| 25 | + |
| 26 | +const config: CliCommandConfig = { |
| 27 | + commandName: 'dotnet', |
| 28 | + description: |
| 29 | + '[beta] Generate Socket facts files from .NET solutions and projects', |
| 30 | + hidden: false, |
| 31 | + flags: { |
| 32 | + ...commonFlags, |
| 33 | + bin: { |
| 34 | + type: 'string', |
| 35 | + description: |
| 36 | + 'Location of the dotnet binary to use, default: dotnet on PATH', |
| 37 | + }, |
| 38 | + dotnetOpts: { |
| 39 | + type: 'string', |
| 40 | + description: |
| 41 | + 'Additional options to pass on to `dotnet restore`, e.g. `-p:Configuration=Release --configfile nuget.config`. MSBuild properties among them also apply to project evaluation', |
| 42 | + }, |
| 43 | + includeConfigs: { |
| 44 | + type: 'string', |
| 45 | + description: |
| 46 | + 'Comma-separated glob patterns matched against target frameworks (case-sensitive; `*`, `?`, and `[...]` wildcards). Only target frameworks matching at least one pattern are read, e.g. `net8.0`. A runtime-specific target (`net8.0/win-x64`) also matches its framework. Default: every target framework', |
| 47 | + }, |
| 48 | + excludeConfigs: { |
| 49 | + type: 'string', |
| 50 | + description: |
| 51 | + 'Comma-separated glob patterns; target frameworks matching any pattern are skipped (applied after --include-configs)', |
| 52 | + }, |
| 53 | + ...excludePathsFlag, |
| 54 | + ignoreUnresolved: { |
| 55 | + type: 'boolean', |
| 56 | + description: |
| 57 | + 'Warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)', |
| 58 | + }, |
| 59 | + verbose: { |
| 60 | + type: 'boolean', |
| 61 | + description: 'Print debug messages', |
| 62 | + }, |
| 63 | + }, |
| 64 | + help: (command, config) => ` |
| 65 | + Usage |
| 66 | + $ ${command} [options] [CWD=.|FILE] |
| 67 | +
|
| 68 | + Options |
| 69 | + ${getFlagListOutput(config.flags)} |
| 70 | +
|
| 71 | + Emits a \`<file>.socket.facts.json\` per solution or project describing its |
| 72 | + resolved NuGet dependency graph. Given a directory, it builds every |
| 73 | + solution (.sln, .slnx) and project (.csproj, .fsproj, .vbproj) at its top |
| 74 | + level, solutions first, skipping projects a solution already covers; given |
| 75 | + a file, just that one (a .slnf solution filter too). |
| 76 | +
|
| 77 | + Dependencies come from your own \`dotnet restore\` (same SDK, global.json, |
| 78 | + nuget.config and credential providers), so a build that restores for you |
| 79 | + restores here. PackageReference projects (central package management and |
| 80 | + lock files included) and packages.config projects are supported. An |
| 81 | + unresolved dependency is a fatal error unless --ignore-unresolved. |
| 82 | +
|
| 83 | + Support is beta. Please report issues or give us feedback on what's missing. |
| 84 | +
|
| 85 | + Examples |
| 86 | +
|
| 87 | + $ ${command} . |
| 88 | + $ ${command} ./App.sln |
| 89 | + $ ${command} --dotnet-opts="-p:Configuration=Release" . |
| 90 | + `, |
| 91 | +} |
| 92 | + |
| 93 | +export const cmdManifestDotnet = { |
| 94 | + description: config.description, |
| 95 | + hidden: config.hidden, |
| 96 | + run, |
| 97 | +} |
| 98 | + |
| 99 | +async function run( |
| 100 | + argv: string[] | readonly string[], |
| 101 | + importMeta: ImportMeta, |
| 102 | + { parentName }: CliCommandContext, |
| 103 | +): Promise<void> { |
| 104 | + const cli = meowOrExit({ |
| 105 | + argv, |
| 106 | + config, |
| 107 | + importMeta, |
| 108 | + parentName, |
| 109 | + }) |
| 110 | + |
| 111 | + const { json = false, markdown = false } = cli.flags |
| 112 | + |
| 113 | + const dryRun = !!cli.flags['dryRun'] |
| 114 | + |
| 115 | + const outputKind = getOutputKind(json, markdown) |
| 116 | + |
| 117 | + const wasValidInput = checkCommandInput(outputKind, { |
| 118 | + nook: true, |
| 119 | + test: cli.input.length <= 1, |
| 120 | + message: 'Can only accept one DIR or FILE (make sure to escape spaces!)', |
| 121 | + fail: 'received ' + cli.input.length, |
| 122 | + }) |
| 123 | + if (!wasValidInput) { |
| 124 | + return |
| 125 | + } |
| 126 | + |
| 127 | + const target = path.resolve(process.cwd(), cli.input[0] ?? '.') |
| 128 | + let isFile = false |
| 129 | + try { |
| 130 | + isFile = statSync(target).isFile() |
| 131 | + } catch {} |
| 132 | + const cwd = isFile ? path.dirname(target) : target |
| 133 | + const entryFile = isFile ? path.basename(target) : undefined |
| 134 | + |
| 135 | + const sockJson = readOrDefaultSocketJson(cwd) |
| 136 | + const defaults = sockJson.defaults?.manifest?.dotnet |
| 137 | + |
| 138 | + debugFn('inspect', `override: ${SOCKET_JSON} dotnet`, defaults) |
| 139 | + |
| 140 | + let { |
| 141 | + bin, |
| 142 | + dotnetOpts, |
| 143 | + excludeConfigs, |
| 144 | + ignoreUnresolved, |
| 145 | + includeConfigs, |
| 146 | + verbose, |
| 147 | + } = cli.flags |
| 148 | + |
| 149 | + function useDefault<T>(flag: string, value: T): T { |
| 150 | + logger.info(`Using default --${flag} from ${SOCKET_JSON}:`, value) |
| 151 | + return value |
| 152 | + } |
| 153 | + if (!bin) { |
| 154 | + bin = defaults?.bin |
| 155 | + ? useDefault('bin', defaults.bin) |
| 156 | + : resolveBuildToolBin('dotnet', cwd) |
| 157 | + } |
| 158 | + if (!dotnetOpts) { |
| 159 | + dotnetOpts = defaults?.dotnetOpts |
| 160 | + ? useDefault('dotnet-opts', defaults.dotnetOpts) |
| 161 | + : '' |
| 162 | + } |
| 163 | + if (includeConfigs === undefined) { |
| 164 | + includeConfigs = defaults?.includeConfigs |
| 165 | + ? useDefault('include-configs', defaults.includeConfigs) |
| 166 | + : '' |
| 167 | + } |
| 168 | + if (excludeConfigs === undefined) { |
| 169 | + excludeConfigs = defaults?.excludeConfigs |
| 170 | + ? useDefault('exclude-configs', defaults.excludeConfigs) |
| 171 | + : '' |
| 172 | + } |
| 173 | + if (ignoreUnresolved === undefined) { |
| 174 | + ignoreUnresolved = |
| 175 | + defaults?.ignoreUnresolved !== undefined |
| 176 | + ? useDefault('ignore-unresolved', defaults.ignoreUnresolved) |
| 177 | + : false |
| 178 | + } |
| 179 | + if (verbose === undefined) { |
| 180 | + verbose = |
| 181 | + defaults?.verbose !== undefined |
| 182 | + ? useDefault('verbose', defaults.verbose) |
| 183 | + : false |
| 184 | + } |
| 185 | + |
| 186 | + if (verbose) { |
| 187 | + logger.group('- ', parentName, config.commandName, ':') |
| 188 | + logger.group('- flags:', cli.flags) |
| 189 | + logger.groupEnd() |
| 190 | + logger.log('- input:', cli.input) |
| 191 | + logger.groupEnd() |
| 192 | + logger.group() |
| 193 | + logger.info('- cwd:', cwd) |
| 194 | + logger.info('- entry:', entryFile ?? '(every solution and project)') |
| 195 | + logger.info('- dotnet bin:', bin) |
| 196 | + logger.groupEnd() |
| 197 | + } |
| 198 | + |
| 199 | + if (dryRun) { |
| 200 | + logger.log(constants.DRY_RUN_BAILING_NOW) |
| 201 | + return |
| 202 | + } |
| 203 | + |
| 204 | + const excludePaths = cmdFlagValueToArray(cli.flags['excludePaths']) |
| 205 | + assertValidExcludePaths(excludePaths) |
| 206 | + |
| 207 | + await convertDotnetToFacts({ |
| 208 | + bin: String(bin), |
| 209 | + cwd, |
| 210 | + dotnetOpts: parseBuildToolOpts(String(dotnetOpts || '')), |
| 211 | + entryFile, |
| 212 | + excludeConfigs: String(excludeConfigs || ''), |
| 213 | + excludePaths, |
| 214 | + ignoreUnresolved: Boolean(ignoreUnresolved), |
| 215 | + includeConfigs: String(includeConfigs || ''), |
| 216 | + verbose: Boolean(verbose), |
| 217 | + }) |
| 218 | +} |
0 commit comments