From 923645efb73ca349406d463298ea3612a84e588b Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Wed, 5 Feb 2025 15:47:55 +0100 Subject: [PATCH 01/10] Add `socket sbom` command and `socket sbom scala` --- README.md | 12 +++ src/commands/index.ts | 1 + src/commands/sbom/index.ts | 60 +++++++++++ src/commands/sbom/scala.ts | 202 +++++++++++++++++++++++++++++++++++++ 4 files changed, 275 insertions(+) create mode 100644 src/commands/sbom/index.ts create mode 100644 src/commands/sbom/scala.ts diff --git a/README.md b/README.md index ec390890..db44dd57 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,18 @@ use of the `projectIgnorePaths` to excludes files when creating a report. ## Contributing +### Setup + +To run dev locally you can run these steps + +``` +npm install +npm run build:dist +npm exec socket +``` + +That should invoke it from local sources. If you make changes you run `build:dist` again. + ### Environment variables for development - `SOCKET_SECURITY_API_BASE_URL` - if set, this will be the base for all diff --git a/src/commands/index.ts b/src/commands/index.ts index fa1aed70..6f03b704 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -18,3 +18,4 @@ export * from './dependencies' export * from './analytics' export * from './diff-scan' export * from './threat-feed' +export * from './sbom' diff --git a/src/commands/sbom/index.ts b/src/commands/sbom/index.ts new file mode 100644 index 00000000..111190ce --- /dev/null +++ b/src/commands/sbom/index.ts @@ -0,0 +1,60 @@ +import meow from 'meow' +import { meowWithSubcommands } from '../../utils/meow-with-subcommands' + +import { scala } from './scala' + +import type { CliSubcommand } from '../../utils/meow-with-subcommands' + +const description = 'Generate a "Software Bill of Materials" for given file or dir' +const help = (name:string) => ` + Usage + + $ ${name} + + Generates the SBOM ("Software Bill of Materials") like a package.json for + Node.JS or requirements.txt for PyPi, but for certain supported ecosystems + where it's common to use a dynamic SBOM definition, like Scala's sbt. + + Only certain languages are supported and there may be language specific + configurations available. See \`sbom --help\` for usage details + per language. + + Currently supported language: scala + + Examples + + $ ${name} ./build.sbt +`; + +export const sbom: CliSubcommand = { + description, + async run(argv, importMeta, { parentName }) { + const name = `${parentName} sbom` + + // Note: this won't catch `socket sbom -xyz --help` sort of cases which + // would fallback to the default meow help behavior. That's fine. + if (argv.length === 0 || argv[0] === '--help') { + meow( + help(name), + { + argv: ['--help'] as const, // meow will exit() when --help is passed + description, + importMeta + } + ) + } + + // argv = argv.filter(o => o !== '--help'); + await meowWithSubcommands( + { + scala + }, + { + argv, + description, + importMeta, + name + } + ) + } +} diff --git a/src/commands/sbom/scala.ts b/src/commands/sbom/scala.ts new file mode 100644 index 00000000..c4bbff41 --- /dev/null +++ b/src/commands/sbom/scala.ts @@ -0,0 +1,202 @@ +import util from 'node:util'; +import fs from 'node:fs'; +import child_process from 'node:child_process'; +import path from 'node:path'; + +import meow from 'meow' +import { getFlagListOutput } from '../../utils/output-formatting.ts' +import { Spinner } from '@socketsecurity/registry/lib/spinner' + +import type { CliSubcommand } from '../../utils/meow-with-subcommands' + +type ListDescription = string | { description: string, type?: string, default?: string } + +const execp = util.promisify(child_process.exec); +const renamep = util.promisify(fs.rename); + +const description = 'Generate a "Software Bill of Materials" (`pom.xml`) from Scala\'s `build.sbt` file' + +const sbomFlags: Record = { + bin: { + type: 'string', + default: 'sbt', + description: 'Location of sbt binary to use' + }, + out: { + type: 'string', + default: './socket.pom.xml', + description: 'Path of output file; where to store the resulting sbom, see also --stdout' + }, + stdout: { + type: 'boolean', + description: 'Print resulting pom.xml to stdout (supersedes --out)' + }, + sbtOpts: { + type: 'string', + default: '', + description: 'Additional options to pass on to sbt, as per `sbt --help`' + }, + verbose: { + type: 'boolean', + description: 'Print debug messages' + } +} + +const help = (name:string, flags: Record) => ` + Usage + $ ${name} [--sbt=path/to/sbt/binary] [--out=path/to/result] FILE|DIR + + Options + ${getFlagListOutput(flags, 6)} + + Uses \`sbt makePom\` to generate a \`pom.xml\` from your \`build.sbt\` file. + This xml file is the SBOM ("Software Bill of Materials") like a package.json + for Node.js or requirements.txt for PyPi, but specifically for Scala. + + There are some caveats with \`build.sbt\` to \`pom.xml\` conversion: + + - the xml is exported as socket.pom.xml as to not confuse existing build tools + but it will first hit your /target/sbt folder (as a different name) + + - the pom.xml format (standard by Scala) does not support certain sbt features + - \`excludeAll()\`, \`dependencyOverrides\`, \`force()\`, \`relativePath\` + - For details: https://www.scala-sbt.org/1.x/docs/Library-Management.html + + - it uses your sbt settings and local configuration verbatim + + - it can only export one target per run, so if you have multiple targets like + development and production, you must run them separately. + + You can optionally configure the path to the \`sbt\` bin to invoke. + + Examples + + $ ${name} ./build.sbt + $ ${name} --bin=/usr/bin/sbt ./build.sbt +`; + +export const scala: CliSubcommand = { + description, + async run(argv, importMeta, { parentName }) { + const name = `${parentName} scala` + // note: meow will exit if it prints the --help screen + const cli = meow( + help(name, sbomFlags), + { + argv: argv.length === 0 ? ['--help'] : argv, + description, + importMeta + } + ) + + const target = cli.input[0] + + if (!target) { + // will exit. + new Spinner().start('Parsing...').error(`Failure: Missing FILE|DIR argument. See \`${name} --help\` for details.`); + process.exit(1); + } + + if (cli.input.length > 1) { + // will exit. + new Spinner().start('Parsing...').error(`Failure: Can only accept one FILE or DIR, received ${cli.input.length} (make sure to escape spaces!). See \`${name} --help\` for details.`); + process.exit(1); + } + + let bin:string = 'sbt' + if (cli.flags['bin']) { + bin = cli.flags['bin'] as string + } + + let out:string = './socket.pom.xml' + if (cli.flags['out']) { + out = cli.flags['out'] as string + } + if (cli.flags['stdout']) { + out = '-'; + } + + // TODO: we can make `-` (accept from stdin) work by storing it into /tmp + if (target === '-') { + new Spinner().start('Parsing...').error(`Failure: Currently source code from stdin is not supported. See \`${name} --help\` for details.`); + process.exit(1); + } + + const verbose = cli.flags['verbose'] as boolean ?? false; + + let sbtOpts:Array = []; + if (cli.flags['sbtOpts']) { + sbtOpts = (cli.flags['sbtOpts'] as string).split(' ').map(s => s.trim()).filter(Boolean) + } + + await startConversion(target, bin, out, verbose, sbtOpts); + } +} + +async function startConversion(target: string, bin: string, out: string, verbose: boolean, sbtOpts: Array) { + const spinner = new Spinner(); + + const rbin = path.resolve(bin); + const rtarget = path.resolve(target); + const rout = out === '-' ? '-' : path.resolve(out); + + if (verbose){ + spinner.clear(); + console.log(`- Absolute bin path: \`${rbin}\``); + console.log(`- Absolute target path: \`${rtarget}\``); + console.log(`- Absolute out path: \`${rout}\``); + } + + spinner.start(`Running sbt from \`${bin}\` on \`${target}\`...`) + + try { + // We must now run sbt, pick the generated xml from the /target folder (the stdout should tell you the location upon success) and store it somewhere else. + // TODO: Not sure what this somewhere else might be tbh. + + const output = await execp(bin +` makePom ${sbtOpts.join(' ')}`, {cwd: target || '.'}); + spinner.success(); + if (verbose) { + console.group('sbt stdout:') + console.log(output); + console.groupEnd(); + } + + if (output.stderr) { + spinner.error('There were errors while running sbt'); + // (In verbose mode, stderr was printed above, no need to repeat it) + if (!verbose) console.error(output.stderr); + process.exit(1); + } + + const loc = output.stdout?.match(/Wrote (.*?.pom)\n/)?.[1]?.trim(); + if (!loc) { + spinner.error('There were no errors from sbt but could not find the location of resulting .pom file either'); + process.exit(1); + } + + // Move the pom file to ...? initial cwd? loc will be an absolute path, or dump to stdout + if (out === '-') { + spinner.start('Result:\n```').success(); + console.log(fs.readFileSync(loc, 'utf8')); + console.log('```') + spinner.start().success(`OK`); + } else { + if (verbose) { + spinner.start(`Moving sbom file from \`${loc.replace(/^\/home\/[^\/]*?\//, '~/')}\` to \`${out}\``); + } else { + spinner.start('Moving output pom file'); + } + // TODO: do we prefer fs-extra? renaming can be gnarly on windows and fs-extra's version is better + await renamep(loc, out); + spinner.success(); + spinner.start().success(`OK. File should be available in \`${out}\``); + } + } catch (e) { + spinner.error('There was an unexpected error while running this') + if (verbose) { + console.log(e); + } + process.exit(1); + } + +} From 5f2fcfaf58cdcaaf2678b015fad8ce3eb20f33d0 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Wed, 5 Feb 2025 16:08:56 +0100 Subject: [PATCH 02/10] lint fix/prettier --- README.md | 4 +- src/commands/sbom/index.ts | 20 +++-- src/commands/sbom/scala.ts | 157 ++++++++++++++++++++++--------------- 3 files changed, 104 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index db44dd57..3dc57036 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ socket --help - `socket optimize` - Optimize dependencies with [`@socketregistry`](https://github.com/SocketDev/socket-registry) overrides! _(👀 [our blog post](https://socket.dev/blog/introducing-socket-optimize))_ + - `--pin` - Pin overrides to their latest version. - `--prod` - Add overrides for only production dependencies. @@ -95,7 +96,8 @@ npm run build:dist npm exec socket ``` -That should invoke it from local sources. If you make changes you run `build:dist` again. +That should invoke it from local sources. If you make changes you run +`build:dist` again. ### Environment variables for development diff --git a/src/commands/sbom/index.ts b/src/commands/sbom/index.ts index 111190ce..028ded92 100644 --- a/src/commands/sbom/index.ts +++ b/src/commands/sbom/index.ts @@ -5,8 +5,9 @@ import { scala } from './scala' import type { CliSubcommand } from '../../utils/meow-with-subcommands' -const description = 'Generate a "Software Bill of Materials" for given file or dir' -const help = (name:string) => ` +const description = + 'Generate a "Software Bill of Materials" for given file or dir' +const help = (name: string) => ` Usage $ ${name} @@ -24,7 +25,7 @@ const help = (name:string) => ` Examples $ ${name} ./build.sbt -`; +` export const sbom: CliSubcommand = { description, @@ -34,14 +35,11 @@ export const sbom: CliSubcommand = { // Note: this won't catch `socket sbom -xyz --help` sort of cases which // would fallback to the default meow help behavior. That's fine. if (argv.length === 0 || argv[0] === '--help') { - meow( - help(name), - { - argv: ['--help'] as const, // meow will exit() when --help is passed - description, - importMeta - } - ) + meow(help(name), { + argv: ['--help'] as const, // meow will exit() when --help is passed + description, + importMeta + }) } // argv = argv.filter(o => o !== '--help'); diff --git a/src/commands/sbom/scala.ts b/src/commands/sbom/scala.ts index c4bbff41..103881c7 100644 --- a/src/commands/sbom/scala.ts +++ b/src/commands/sbom/scala.ts @@ -1,7 +1,7 @@ -import util from 'node:util'; -import fs from 'node:fs'; -import child_process from 'node:child_process'; -import path from 'node:path'; +import util from 'node:util' +import fs from 'node:fs' +import child_process from 'node:child_process' +import path from 'node:path' import meow from 'meow' import { getFlagListOutput } from '../../utils/output-formatting.ts' @@ -9,12 +9,15 @@ import { Spinner } from '@socketsecurity/registry/lib/spinner' import type { CliSubcommand } from '../../utils/meow-with-subcommands' -type ListDescription = string | { description: string, type?: string, default?: string } +type ListDescription = + | string + | { description: string; type?: string; default?: string } -const execp = util.promisify(child_process.exec); -const renamep = util.promisify(fs.rename); +const execp = util.promisify(child_process.exec) +const renamep = util.promisify(fs.rename) -const description = 'Generate a "Software Bill of Materials" (`pom.xml`) from Scala\'s `build.sbt` file' +const description = + 'Generate a "Software Bill of Materials" (`pom.xml`) from Scala\'s `build.sbt` file' const sbomFlags: Record = { bin: { @@ -25,7 +28,8 @@ const sbomFlags: Record = { out: { type: 'string', default: './socket.pom.xml', - description: 'Path of output file; where to store the resulting sbom, see also --stdout' + description: + 'Path of output file; where to store the resulting sbom, see also --stdout' }, stdout: { type: 'boolean', @@ -42,7 +46,7 @@ const sbomFlags: Record = { } } -const help = (name:string, flags: Record) => ` +const help = (name: string, flags: Record) => ` Usage $ ${name} [--sbt=path/to/sbt/binary] [--out=path/to/result] FILE|DIR @@ -73,78 +77,96 @@ const help = (name:string, flags: Record) => ` $ ${name} ./build.sbt $ ${name} --bin=/usr/bin/sbt ./build.sbt -`; +` export const scala: CliSubcommand = { description, async run(argv, importMeta, { parentName }) { const name = `${parentName} scala` // note: meow will exit if it prints the --help screen - const cli = meow( - help(name, sbomFlags), - { - argv: argv.length === 0 ? ['--help'] : argv, - description, - importMeta - } - ) + const cli = meow(help(name, sbomFlags), { + argv: argv.length === 0 ? ['--help'] : argv, + description, + importMeta + }) const target = cli.input[0] if (!target) { // will exit. - new Spinner().start('Parsing...').error(`Failure: Missing FILE|DIR argument. See \`${name} --help\` for details.`); - process.exit(1); + new Spinner() + .start('Parsing...') + .error( + `Failure: Missing FILE|DIR argument. See \`${name} --help\` for details.` + ) + process.exit(1) } if (cli.input.length > 1) { // will exit. - new Spinner().start('Parsing...').error(`Failure: Can only accept one FILE or DIR, received ${cli.input.length} (make sure to escape spaces!). See \`${name} --help\` for details.`); - process.exit(1); + new Spinner() + .start('Parsing...') + .error( + `Failure: Can only accept one FILE or DIR, received ${cli.input.length} (make sure to escape spaces!). See \`${name} --help\` for details.` + ) + process.exit(1) } - let bin:string = 'sbt' + let bin: string = 'sbt' if (cli.flags['bin']) { bin = cli.flags['bin'] as string } - let out:string = './socket.pom.xml' + let out: string = './socket.pom.xml' if (cli.flags['out']) { out = cli.flags['out'] as string } if (cli.flags['stdout']) { - out = '-'; + out = '-' } // TODO: we can make `-` (accept from stdin) work by storing it into /tmp if (target === '-') { - new Spinner().start('Parsing...').error(`Failure: Currently source code from stdin is not supported. See \`${name} --help\` for details.`); - process.exit(1); + new Spinner() + .start('Parsing...') + .error( + `Failure: Currently source code from stdin is not supported. See \`${name} --help\` for details.` + ) + process.exit(1) } - const verbose = cli.flags['verbose'] as boolean ?? false; + const verbose = (cli.flags['verbose'] as boolean) ?? false - let sbtOpts:Array = []; + let sbtOpts: Array = [] if (cli.flags['sbtOpts']) { - sbtOpts = (cli.flags['sbtOpts'] as string).split(' ').map(s => s.trim()).filter(Boolean) + sbtOpts = (cli.flags['sbtOpts'] as string) + .split(' ') + .map(s => s.trim()) + .filter(Boolean) } - await startConversion(target, bin, out, verbose, sbtOpts); + await startConversion(target, bin, out, verbose, sbtOpts) } } -async function startConversion(target: string, bin: string, out: string, verbose: boolean, sbtOpts: Array) { - const spinner = new Spinner(); - - const rbin = path.resolve(bin); - const rtarget = path.resolve(target); - const rout = out === '-' ? '-' : path.resolve(out); - - if (verbose){ - spinner.clear(); - console.log(`- Absolute bin path: \`${rbin}\``); - console.log(`- Absolute target path: \`${rtarget}\``); - console.log(`- Absolute out path: \`${rout}\``); +async function startConversion( + target: string, + bin: string, + out: string, + verbose: boolean, + sbtOpts: Array +) { + const spinner = new Spinner() + + const rbin = path.resolve(bin) + const rtarget = path.resolve(target) + const rout = out === '-' ? '-' : path.resolve(out) + + if (verbose) { + spinner.clear() + console.log(`- Absolute bin path: \`${rbin}\``) + console.log(`- Absolute target path: \`${rtarget}\``) + console.log(`- Absolute out path: \`${rout}\``) } spinner.start(`Running sbt from \`${bin}\` on \`${target}\`...`) @@ -153,50 +175,55 @@ async function startConversion(target: string, bin: string, out: string, verbose // We must now run sbt, pick the generated xml from the /target folder (the stdout should tell you the location upon success) and store it somewhere else. // TODO: Not sure what this somewhere else might be tbh. - const output = await execp(bin +` makePom ${sbtOpts.join(' ')}`, {cwd: target || '.'}); - spinner.success(); + const output = await execp(bin + ` makePom ${sbtOpts.join(' ')}`, { + cwd: target || '.' + }) + spinner.success() if (verbose) { console.group('sbt stdout:') - console.log(output); - console.groupEnd(); + console.log(output) + console.groupEnd() } if (output.stderr) { - spinner.error('There were errors while running sbt'); + spinner.error('There were errors while running sbt') // (In verbose mode, stderr was printed above, no need to repeat it) - if (!verbose) console.error(output.stderr); - process.exit(1); + if (!verbose) console.error(output.stderr) + process.exit(1) } - const loc = output.stdout?.match(/Wrote (.*?.pom)\n/)?.[1]?.trim(); + const loc = output.stdout?.match(/Wrote (.*?.pom)\n/)?.[1]?.trim() if (!loc) { - spinner.error('There were no errors from sbt but could not find the location of resulting .pom file either'); - process.exit(1); + spinner.error( + 'There were no errors from sbt but could not find the location of resulting .pom file either' + ) + process.exit(1) } // Move the pom file to ...? initial cwd? loc will be an absolute path, or dump to stdout if (out === '-') { - spinner.start('Result:\n```').success(); - console.log(fs.readFileSync(loc, 'utf8')); + spinner.start('Result:\n```').success() + console.log(fs.readFileSync(loc, 'utf8')) console.log('```') - spinner.start().success(`OK`); + spinner.start().success(`OK`) } else { if (verbose) { - spinner.start(`Moving sbom file from \`${loc.replace(/^\/home\/[^\/]*?\//, '~/')}\` to \`${out}\``); + spinner.start( + `Moving sbom file from \`${loc.replace(/^\/home\/[^/]*?\//, '~/')}\` to \`${out}\`` + ) } else { - spinner.start('Moving output pom file'); + spinner.start('Moving output pom file') } // TODO: do we prefer fs-extra? renaming can be gnarly on windows and fs-extra's version is better - await renamep(loc, out); - spinner.success(); - spinner.start().success(`OK. File should be available in \`${out}\``); + await renamep(loc, out) + spinner.success() + spinner.start().success(`OK. File should be available in \`${out}\``) } } catch (e) { spinner.error('There was an unexpected error while running this') if (verbose) { - console.log(e); + console.log(e) } - process.exit(1); + process.exit(1) } - } From 3356fbea8ec6e264e9f0b32b2e1e387b8fe2f0f3 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 6 Feb 2025 12:21:14 +0100 Subject: [PATCH 03/10] sbom -> manifest --- src/commands/index.ts | 2 +- src/commands/{sbom => manifest}/index.ts | 20 ++++++++++---------- src/commands/{sbom => manifest}/scala.ts | 20 +++++++++++--------- 3 files changed, 22 insertions(+), 20 deletions(-) rename src/commands/{sbom => manifest}/index.ts (65%) rename src/commands/{sbom => manifest}/scala.ts (93%) diff --git a/src/commands/index.ts b/src/commands/index.ts index 6f03b704..b3207e32 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -18,4 +18,4 @@ export * from './dependencies' export * from './analytics' export * from './diff-scan' export * from './threat-feed' -export * from './sbom' +export * from './manifest' diff --git a/src/commands/sbom/index.ts b/src/commands/manifest/index.ts similarity index 65% rename from src/commands/sbom/index.ts rename to src/commands/manifest/index.ts index 028ded92..51308298 100644 --- a/src/commands/sbom/index.ts +++ b/src/commands/manifest/index.ts @@ -1,7 +1,8 @@ import meow from 'meow' -import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import { scala } from './scala' +import { meowWithSubcommands } from '../../utils/meow-with-subcommands' + import type { CliSubcommand } from '../../utils/meow-with-subcommands' @@ -12,27 +13,27 @@ const help = (name: string) => ` $ ${name} - Generates the SBOM ("Software Bill of Materials") like a package.json for - Node.JS or requirements.txt for PyPi, but for certain supported ecosystems - where it's common to use a dynamic SBOM definition, like Scala's sbt. + Generates a declarative dependency manifest (like a package.json for Node.JS + or requirements.txt for PyPi), but for certain supported ecosystems + where it's common to use a dynamic manifest, like Scala's sbt. Only certain languages are supported and there may be language specific - configurations available. See \`sbom --help\` for usage details + configurations available. See \`manifest --help\` for usage details per language. Currently supported language: scala Examples - $ ${name} ./build.sbt + $ ${name} scala . ` -export const sbom: CliSubcommand = { +export const manifest: CliSubcommand = { description, async run(argv, importMeta, { parentName }) { - const name = `${parentName} sbom` + const name = `${parentName} manifest` - // Note: this won't catch `socket sbom -xyz --help` sort of cases which + // Note: this won't catch `socket manifest -xyz --help` sort of cases which // would fallback to the default meow help behavior. That's fine. if (argv.length === 0 || argv[0] === '--help') { meow(help(name), { @@ -42,7 +43,6 @@ export const sbom: CliSubcommand = { }) } - // argv = argv.filter(o => o !== '--help'); await meowWithSubcommands( { scala diff --git a/src/commands/sbom/scala.ts b/src/commands/manifest/scala.ts similarity index 93% rename from src/commands/sbom/scala.ts rename to src/commands/manifest/scala.ts index 103881c7..2c256731 100644 --- a/src/commands/sbom/scala.ts +++ b/src/commands/manifest/scala.ts @@ -1,12 +1,14 @@ -import util from 'node:util' -import fs from 'node:fs' import child_process from 'node:child_process' +import fs from 'node:fs' import path from 'node:path' +import util from 'node:util' import meow from 'meow' -import { getFlagListOutput } from '../../utils/output-formatting.ts' + import { Spinner } from '@socketsecurity/registry/lib/spinner' +import { getFlagListOutput } from '../../utils/output-formatting.ts' + import type { CliSubcommand } from '../../utils/meow-with-subcommands' type ListDescription = @@ -19,7 +21,7 @@ const renamep = util.promisify(fs.rename) const description = 'Generate a "Software Bill of Materials" (`pom.xml`) from Scala\'s `build.sbt` file' -const sbomFlags: Record = { +const scalaCmdFlags: Record = { bin: { type: 'string', default: 'sbt', @@ -29,7 +31,7 @@ const sbomFlags: Record = { type: 'string', default: './socket.pom.xml', description: - 'Path of output file; where to store the resulting sbom, see also --stdout' + 'Path of output file; where to store the resulting manifest, see also --stdout' }, stdout: { type: 'boolean', @@ -54,8 +56,8 @@ const help = (name: string, flags: Record) => ` ${getFlagListOutput(flags, 6)} Uses \`sbt makePom\` to generate a \`pom.xml\` from your \`build.sbt\` file. - This xml file is the SBOM ("Software Bill of Materials") like a package.json - for Node.js or requirements.txt for PyPi, but specifically for Scala. + This xml file is the dependency manifest (like a package.json + for Node.js or requirements.txt for PyPi), but specifically for Scala. There are some caveats with \`build.sbt\` to \`pom.xml\` conversion: @@ -84,7 +86,7 @@ export const scala: CliSubcommand = { async run(argv, importMeta, { parentName }) { const name = `${parentName} scala` // note: meow will exit if it prints the --help screen - const cli = meow(help(name, sbomFlags), { + const cli = meow(help(name, scalaCmdFlags), { argv: argv.length === 0 ? ['--help'] : argv, description, importMeta @@ -209,7 +211,7 @@ async function startConversion( } else { if (verbose) { spinner.start( - `Moving sbom file from \`${loc.replace(/^\/home\/[^/]*?\//, '~/')}\` to \`${out}\`` + `Moving manifest file from \`${loc.replace(/^\/home\/[^/]*?\//, '~/')}\` to \`${out}\`` ) } else { spinner.start('Moving output pom file') From 6a7a6c1a668bba0fe731e752154223afade4b524 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 6 Feb 2025 12:24:59 +0100 Subject: [PATCH 04/10] lint:fix --- src/commands/manifest/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/manifest/index.ts b/src/commands/manifest/index.ts index 51308298..57460b43 100644 --- a/src/commands/manifest/index.ts +++ b/src/commands/manifest/index.ts @@ -3,7 +3,6 @@ import meow from 'meow' import { scala } from './scala' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' - import type { CliSubcommand } from '../../utils/meow-with-subcommands' const description = From 1b1b4e89247a0c60b96a1088c42c565588bd2804 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 6 Feb 2025 14:45:30 +0100 Subject: [PATCH 05/10] Use common readFile/spawn deps --- src/commands/manifest/scala.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/commands/manifest/scala.ts b/src/commands/manifest/scala.ts index 2c256731..8f5cbac2 100644 --- a/src/commands/manifest/scala.ts +++ b/src/commands/manifest/scala.ts @@ -1,12 +1,13 @@ -import child_process from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import util from 'node:util' import meow from 'meow' +import spawn from '@npmcli/promise-spawn' import { Spinner } from '@socketsecurity/registry/lib/spinner' +import { safeReadFile } from '../../utils/fs' import { getFlagListOutput } from '../../utils/output-formatting.ts' import type { CliSubcommand } from '../../utils/meow-with-subcommands' @@ -15,7 +16,6 @@ type ListDescription = | string | { description: string; type?: string; default?: string } -const execp = util.promisify(child_process.exec) const renamep = util.promisify(fs.rename) const description = @@ -177,7 +177,7 @@ async function startConversion( // We must now run sbt, pick the generated xml from the /target folder (the stdout should tell you the location upon success) and store it somewhere else. // TODO: Not sure what this somewhere else might be tbh. - const output = await execp(bin + ` makePom ${sbtOpts.join(' ')}`, { + const output = await spawn(bin, ['makePom'].concat(sbtOpts), { cwd: target || '.' }) spinner.success() @@ -205,7 +205,7 @@ async function startConversion( // Move the pom file to ...? initial cwd? loc will be an absolute path, or dump to stdout if (out === '-') { spinner.start('Result:\n```').success() - console.log(fs.readFileSync(loc, 'utf8')) + console.log(await safeReadFile(loc, 'utf8')) console.log('```') spinner.start().success(`OK`) } else { From e89a9210da5a3b77077d2531fd9085e24ee57713 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Thu, 6 Feb 2025 14:47:21 +0100 Subject: [PATCH 06/10] lint --- src/commands/manifest/scala.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/manifest/scala.ts b/src/commands/manifest/scala.ts index 8f5cbac2..8ba2ad78 100644 --- a/src/commands/manifest/scala.ts +++ b/src/commands/manifest/scala.ts @@ -2,9 +2,9 @@ import fs from 'node:fs' import path from 'node:path' import util from 'node:util' +import spawn from '@npmcli/promise-spawn' import meow from 'meow' -import spawn from '@npmcli/promise-spawn' import { Spinner } from '@socketsecurity/registry/lib/spinner' import { safeReadFile } from '../../utils/fs' From 875eb61c9631f773dc2ff5355e0ddffda19a3147 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Fri, 7 Feb 2025 15:42:10 +0100 Subject: [PATCH 07/10] Add a `manifest auto` command which tries to zero config generate a manifest --- src/commands/manifest/auto.ts | 74 ++++++++++++++++++++++++++++++ src/commands/manifest/index.ts | 18 +++++++- src/commands/manifest/scala.ts | 34 ++++++++++---- src/utils/meow-with-subcommands.ts | 3 +- 4 files changed, 116 insertions(+), 13 deletions(-) create mode 100644 src/commands/manifest/auto.ts diff --git a/src/commands/manifest/auto.ts b/src/commands/manifest/auto.ts new file mode 100644 index 00000000..ce348049 --- /dev/null +++ b/src/commands/manifest/auto.ts @@ -0,0 +1,74 @@ +import fs from 'node:fs' + +import meow from 'meow' + +import type { CliSubcommand } from '../../utils/meow-with-subcommands' +import { scala } from './scala.ts' + +const description = + 'Auto-detect build and attempt to generate manifest file' + +const help = (name: string) => ` + Usage + $ ${name} + + Tries to figure out what language your current repo uses. If it finds a + supported case then it will try to generate the manifest file for that + language with the default or detected settings. + + This command takes no arguments except --verbose. +` + +export const auto: CliSubcommand = { + description, + async run(argv, importMeta, { parentName }) { + // Allow `--verbose` to pass through + let verbose = false; + const args = argv.filter(arg => { + if (arg === '--verbose') { + verbose = true; + return false; + } + return true; + }); + + const name = `${parentName} auto` + if (args.length) { + // note: meow will exit if it prints the --help screen + meow(help(name), { + argv: ['--help'], + description, + importMeta + }) + } + + const subArgs = []; + if (verbose) subArgs.push('--verbose', '1'); + const scalaDir = '.'; + if (fs.existsSync(scalaDir)) { + console.log('Detected a Scala sbt build, running default Scala generator...') + subArgs.push(scalaDir) + await scala.run(subArgs, importMeta, {parentName}) + return; + } + + // Show new help screen and exit + meow(` + $ ${name} + + Unfortunately this script did not discover a supported language in the + current folder. + + - Make sure this script would work with your target build + - Make sure to run it from the correct folder + - Make sure the necessary build tools are available (\`PATH\`) + + If that doesn't work, see \`${name} --help\` for config details + `, { + argv: ['--help'], + description, + importMeta + }) + } +} + diff --git a/src/commands/manifest/index.ts b/src/commands/manifest/index.ts index 57460b43..53a3daf0 100644 --- a/src/commands/manifest/index.ts +++ b/src/commands/manifest/index.ts @@ -4,9 +4,10 @@ import { scala } from './scala' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import type { CliSubcommand } from '../../utils/meow-with-subcommands' +import { auto } from './auto.ts' const description = - 'Generate a "Software Bill of Materials" for given file or dir' + 'Generate a dependency manifest for given file or dir' const help = (name: string) => ` Usage @@ -25,10 +26,15 @@ const help = (name: string) => ` Examples $ ${name} scala . + + To have it auto-detect and attempt to run: + + $ ${name} yolo ` export const manifest: CliSubcommand = { description, + hidden: true, async run(argv, importMeta, { parentName }) { const name = `${parentName} manifest` @@ -44,10 +50,18 @@ export const manifest: CliSubcommand = { await meowWithSubcommands( { - scala + scala, + auto }, { argv, + aliases: { + yolo: { + description: auto.description, + hidden: true, + argv: ['auto'] + } + }, description, importMeta, name diff --git a/src/commands/manifest/scala.ts b/src/commands/manifest/scala.ts index 8ba2ad78..d2c313e3 100644 --- a/src/commands/manifest/scala.ts +++ b/src/commands/manifest/scala.ts @@ -19,7 +19,7 @@ type ListDescription = const renamep = util.promisify(fs.rename) const description = - 'Generate a "Software Bill of Materials" (`pom.xml`) from Scala\'s `build.sbt` file' + 'Generate a manifest file (`pom.xml`) from Scala\'s `build.sbt` file' const scalaCmdFlags: Record = { bin: { @@ -84,6 +84,7 @@ const help = (name: string, flags: Record) => ` export const scala: CliSubcommand = { description, async run(argv, importMeta, { parentName }) { + // console.log('scala', argv, parentName) const name = `${parentName} scala` // note: meow will exit if it prints the --help screen const cli = meow(help(name, scalaCmdFlags), { @@ -92,6 +93,10 @@ export const scala: CliSubcommand = { importMeta }) + if (cli.flags['verbose']) { + console.log('[VERBOSE] cli.flags:', cli.flags, ', cli.input:', cli.input) + } + const target = cli.input[0] if (!target) { @@ -158,19 +163,22 @@ async function startConversion( verbose: boolean, sbtOpts: Array ) { - const spinner = new Spinner() - const rbin = path.resolve(bin) const rtarget = path.resolve(target) const rout = out === '-' ? '-' : path.resolve(out) if (verbose) { - spinner.clear() - console.log(`- Absolute bin path: \`${rbin}\``) - console.log(`- Absolute target path: \`${rtarget}\``) - console.log(`- Absolute out path: \`${rout}\``) + console.log(`[VERBOSE] - Absolute bin path: \`${rbin}\``) + console.log(`[VERBOSE] - Absolute target path: \`${rtarget}\``) + console.log(`[VERBOSE] - Absolute out path: \`${rout}\``) + } else { + console.log(`- executing: \`${bin}\``); + console.log(`- src dir: \`${target}\``) + console.log(`- dst dir: \`${out}\``) } + const spinner = new Spinner() + spinner.start(`Running sbt from \`${bin}\` on \`${target}\`...`) try { @@ -182,7 +190,7 @@ async function startConversion( }) spinner.success() if (verbose) { - console.group('sbt stdout:') + console.group('[VERBOSE] sbt stdout:') console.log(output) console.groupEnd() } @@ -190,7 +198,11 @@ async function startConversion( if (output.stderr) { spinner.error('There were errors while running sbt') // (In verbose mode, stderr was printed above, no need to repeat it) - if (!verbose) console.error(output.stderr) + if (!verbose) { + console.group('[VERBOSE] stderr:'); + console.error(output.stderr) + console.groupEnd(); + } process.exit(1) } @@ -222,9 +234,11 @@ async function startConversion( spinner.start().success(`OK. File should be available in \`${out}\``) } } catch (e) { - spinner.error('There was an unexpected error while running this') + spinner.error('There was an unexpected error while running this' + (verbose ? '' : ' (use --verbose for details)')) if (verbose) { + console.group('[VERBOSE] error:'); console.log(e) + console.groupEnd(); } process.exit(1) } diff --git a/src/utils/meow-with-subcommands.ts b/src/utils/meow-with-subcommands.ts index b97c64c4..40d8159d 100644 --- a/src/utils/meow-with-subcommands.ts +++ b/src/utils/meow-with-subcommands.ts @@ -9,6 +9,7 @@ import type { Options } from 'meow' interface CliAlias { description: string + hidden?: boolean argv: readonly string[] } @@ -81,7 +82,7 @@ export async function meowWithSubcommands( ...toSortedObject( Object.fromEntries( Object.entries(aliases).filter( - entry => !subcommands[entry[1]?.argv[0]!]?.hidden + entry => !entry[1]?.hidden && !subcommands[entry[1]?.argv[0]!]?.hidden ) ) ) From 3cd83e9458dff425b2269dacb28315ebc649b7be Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Mon, 10 Feb 2025 13:44:16 +0100 Subject: [PATCH 08/10] Back out change that landed in main already --- src/utils/meow-with-subcommands.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/utils/meow-with-subcommands.ts b/src/utils/meow-with-subcommands.ts index 40d8159d..b97c64c4 100644 --- a/src/utils/meow-with-subcommands.ts +++ b/src/utils/meow-with-subcommands.ts @@ -9,7 +9,6 @@ import type { Options } from 'meow' interface CliAlias { description: string - hidden?: boolean argv: readonly string[] } @@ -82,7 +81,7 @@ export async function meowWithSubcommands( ...toSortedObject( Object.fromEntries( Object.entries(aliases).filter( - entry => !entry[1]?.hidden && !subcommands[entry[1]?.argv[0]!]?.hidden + entry => !subcommands[entry[1]?.argv[0]!]?.hidden ) ) ) From 59f5e4310d62de6ad7be4850d717d7b7e0fe7c3a Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Mon, 10 Feb 2025 17:22:31 +0100 Subject: [PATCH 09/10] Pass on flag options to meow proper, fixing boolean flags --- src/commands/manifest/scala.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/commands/manifest/scala.ts b/src/commands/manifest/scala.ts index d2c313e3..52b28301 100644 --- a/src/commands/manifest/scala.ts +++ b/src/commands/manifest/scala.ts @@ -19,7 +19,7 @@ type ListDescription = const renamep = util.promisify(fs.rename) const description = - 'Generate a manifest file (`pom.xml`) from Scala\'s `build.sbt` file' + "Generate a manifest file (`pom.xml`) from Scala's `build.sbt` file" const scalaCmdFlags: Record = { bin: { @@ -88,8 +88,10 @@ export const scala: CliSubcommand = { const name = `${parentName} scala` // note: meow will exit if it prints the --help screen const cli = meow(help(name, scalaCmdFlags), { + flags: <{ [key: string]: any }>scalaCmdFlags, argv: argv.length === 0 ? ['--help'] : argv, description, + allowUnknownFlags: false, importMeta }) @@ -98,7 +100,6 @@ export const scala: CliSubcommand = { } const target = cli.input[0] - if (!target) { // will exit. new Spinner() @@ -172,7 +173,7 @@ async function startConversion( console.log(`[VERBOSE] - Absolute target path: \`${rtarget}\``) console.log(`[VERBOSE] - Absolute out path: \`${rout}\``) } else { - console.log(`- executing: \`${bin}\``); + console.log(`- executing: \`${bin}\``) console.log(`- src dir: \`${target}\``) console.log(`- dst dir: \`${out}\``) } @@ -199,9 +200,9 @@ async function startConversion( spinner.error('There were errors while running sbt') // (In verbose mode, stderr was printed above, no need to repeat it) if (!verbose) { - console.group('[VERBOSE] stderr:'); + console.group('[VERBOSE] stderr:') console.error(output.stderr) - console.groupEnd(); + console.groupEnd() } process.exit(1) } @@ -234,11 +235,14 @@ async function startConversion( spinner.start().success(`OK. File should be available in \`${out}\``) } } catch (e) { - spinner.error('There was an unexpected error while running this' + (verbose ? '' : ' (use --verbose for details)')) + spinner.error( + 'There was an unexpected error while running this' + + (verbose ? '' : ' (use --verbose for details)') + ) if (verbose) { - console.group('[VERBOSE] error:'); + console.group('[VERBOSE] error:') console.log(e) - console.groupEnd(); + console.groupEnd() } process.exit(1) } From 82ea91b331fa1b68879bdc8e05689d9345794957 Mon Sep 17 00:00:00 2001 From: Peter van der Zee Date: Mon, 10 Feb 2025 17:25:53 +0100 Subject: [PATCH 10/10] npm run check:lint -- --fix --- src/commands/manifest/auto.ts | 46 ++++++++++++++++++---------------- src/commands/manifest/index.ts | 5 ++-- 2 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/commands/manifest/auto.ts b/src/commands/manifest/auto.ts index ce348049..7793adf5 100644 --- a/src/commands/manifest/auto.ts +++ b/src/commands/manifest/auto.ts @@ -2,11 +2,11 @@ import fs from 'node:fs' import meow from 'meow' -import type { CliSubcommand } from '../../utils/meow-with-subcommands' import { scala } from './scala.ts' -const description = - 'Auto-detect build and attempt to generate manifest file' +import type { CliSubcommand } from '../../utils/meow-with-subcommands' + +const description = 'Auto-detect build and attempt to generate manifest file' const help = (name: string) => ` Usage @@ -23,14 +23,14 @@ export const auto: CliSubcommand = { description, async run(argv, importMeta, { parentName }) { // Allow `--verbose` to pass through - let verbose = false; + let verbose = false const args = argv.filter(arg => { if (arg === '--verbose') { - verbose = true; - return false; + verbose = true + return false } - return true; - }); + return true + }) const name = `${parentName} auto` if (args.length) { @@ -42,18 +42,21 @@ export const auto: CliSubcommand = { }) } - const subArgs = []; - if (verbose) subArgs.push('--verbose', '1'); - const scalaDir = '.'; + const subArgs = [] + if (verbose) subArgs.push('--verbose', '1') + const scalaDir = '.' if (fs.existsSync(scalaDir)) { - console.log('Detected a Scala sbt build, running default Scala generator...') + console.log( + 'Detected a Scala sbt build, running default Scala generator...' + ) subArgs.push(scalaDir) - await scala.run(subArgs, importMeta, {parentName}) - return; + await scala.run(subArgs, importMeta, { parentName }) + return } // Show new help screen and exit - meow(` + meow( + ` $ ${name} Unfortunately this script did not discover a supported language in the @@ -64,11 +67,12 @@ export const auto: CliSubcommand = { - Make sure the necessary build tools are available (\`PATH\`) If that doesn't work, see \`${name} --help\` for config details - `, { - argv: ['--help'], - description, - importMeta - }) + `, + { + argv: ['--help'], + description, + importMeta + } + ) } } - diff --git a/src/commands/manifest/index.ts b/src/commands/manifest/index.ts index 53a3daf0..6cc5a462 100644 --- a/src/commands/manifest/index.ts +++ b/src/commands/manifest/index.ts @@ -1,13 +1,12 @@ import meow from 'meow' +import { auto } from './auto.ts' import { scala } from './scala' import { meowWithSubcommands } from '../../utils/meow-with-subcommands' import type { CliSubcommand } from '../../utils/meow-with-subcommands' -import { auto } from './auto.ts' -const description = - 'Generate a dependency manifest for given file or dir' +const description = 'Generate a dependency manifest for given file or dir' const help = (name: string) => ` Usage