Skip to content

Commit 07bbf95

Browse files
authored
Add socket manifest dynamic-sbom-inference (#1467)
* Add socket manifest dynamic-sbom-inference Recursively discovers independent gradle/sbt/maven build roots and generates a Socket facts SBOM for each, without re-invoking the manifest script on subproject/reactor-module directories a parent build root already covers. Maven reactors in particular have a pom.xml per module, so naive marker-file discovery would over-invoke; coverage is tracked per ecosystem using the facts SBOM's own projects[].subprojectDir, which every build-tool producer already reports. This is a standalone command rather than an --auto-manifest extension, so it can force stricter behavior later without touching auto's existing contract. Scoped narrowly for this first PR: discovery plus generation only, one global socket.json config applied to every discovered root (no per-build-root cascade), no new CLI flags beyond --exclude-paths/--verbose. Refs REA-685, REA-553 * Fix misleading --exclude-paths description on manifest commands --exclude-paths on socket manifest auto/gradle/kotlin/maven/scala/ dynamic-sbom-inference reused the flag description from the scan/reach flag definitions verbatim, which talks about "the scan" and full application reachability analysis. None of these commands scan or run reachability analysis themselves, so the wording was confusing when viewed via --help on any of them standalone. Adds a manifest-scoped excludePathsFlag with wording specific to manifest/facts generation and switches all six commands to it. * Add per-ecosystem javaHome config for gradle/maven/sbt manifest generation Different projects in a repo may need different JDKs for their build tool. Adds a javaHome field to defaults.manifest.{gradle,maven,sbt} in socket.json, configurable via the socket manifest setup wizard, and threads it through to the actual build-tool invocation (overrides JAVA_HOME for that spawn only, everything else about the environment is left untouched). Applies to socket manifest gradle/kotlin/maven/scala, socket manifest auto, and socket manifest dynamic-sbom-inference. Scoped to the Socket facts generation path only, not the legacy --pom conversion path. No new CLI flag - socket.json/the setup wizard is the only configuration surface for now. * Resolve socket.json per build root, and support env var refs in javaHome socket manifest dynamic-sbom-inference previously read socket.json once at the overall recursion root and applied that single config to every discovered build root. It now resolves each build root's own nearest socket.json (walking up from that root, bounded at the recursion root, nearest wins - no merging), so different projects in the same repo can carry their own settings instead of being forced onto one shared config. Also adds $VAR/${VAR} expansion for javaHome, resolved against the CLI process's own environment. A hardcoded absolute JDK path only works on whoever's machine wrote it; referencing an env var each developer sets themselves (e.g. $JAVA11_HOME) makes a shared socket.json portable across machines. Fails closed with a clear message if the referenced variable isn't set, rather than silently passing a broken path to the build tool. * Deep-merge the socket.json cascade instead of nearest-wins readSocketJsonCascade (renamed from readOrDefaultSocketJsonUpTo) now merges defaults.manifest.<ecosystem> field-by-field across every ancestor between a build root and the recursion root, nearest winning per field, instead of one file replacing the root config wholesale. A subproject can now override just javaHome while still inheriting the root's excludeConfigs/bin/etc., rather than having to restate the whole config. Ecosystems the override doesn't mention are left untouched. Verified against the sandbox tree: a root socket.json setting excludeConfigs plus a nested one setting only javaHome both applied together for that build root. * Warn (but proceed) when a resolved config sets facts: false dynamic-sbom-inference always generates Socket facts SBOMs, never pom.xml. If a build root's cascaded socket.json sets facts: false (the pom-mode opt-out other manifest commands honor), that's ignored here rather than skipping the project or silently doing nothing - but it's a real, deliberate setting the user made, so it's surfaced as a warning rather than silently overridden. * Revert facts:false to skip, add a dedicated ignored config dynamic-sbom-inference previously forced Socket facts generation over an explicit defaults.manifest.<ecosystem>.facts: false (pom mode), just warning about it. Reverted: facts: false now skips the project again, same as before that change, since this command has no pom-mode equivalent to fall back to. Also adds a separate ignored: true field (gradle/maven/sbt) scoped specifically to dynamic-sbom-inference, for projects that should be skipped during recursive generation regardless of their facts/pom preference for other commands. Kept as its own boolean rather than folding into facts, since facts already means something specific to other manifest commands and overloading it would require touching every consumer for a value only this command understands. New skippedIgnored outcome status covers both reasons, distinguished via the warning message. * Consolidate ignored into disabled instead of a separate field A cascaded (not just root-level) disabled: true produces identical per-root behavior to the ignored field just added, so keeping both was redundant. Removed ignored; disabled now does double duty: root-only ecosystem-wide gating for auto/gradle/etc. (unchanged), plus a cascaded per-build-root skip specifically for dynamic-sbom-inference. Renamed the skippedIgnored outcome status to skippedDisabled to match. * Add explicit clear sentinel, lightweight workspace discovery, and recursive setup for dynamic-sbom-inference - socket.json manifest fields now accept `null` as an explicit "clear the inherited value" sentinel, distinct from leaving a field unset; the setup wizard writes it when a previously-set value is cleared instead of just deleting the key. - Add a lightweight per-ecosystem workspace enumeration path for gradle/sbt/maven (new standalone scripts, kept fully separate from the existing facts-generation scripts) that discovers a build's subprojects without running dependency resolution. - Add a hidden `socket manifest setup --dynamic-sbom-inference` mode: configures root-level defaults per ecosystem, then recursively marks `disabled: true` on build roots matching `--exclude-paths`, leaving everything else untouched. - Speed up `dynamic-sbom-inference`'s handling of a disabled build root with many nested candidates by reusing the nearest already-resolved disabled ancestor instead of re-walking the whole config cascade for each one, and only logging the root cause instead of once per nested candidate. * Write --exclude-paths disable at the exclusion root, not per project Previously each excluded build root got its own disabled:true write, relying on cascade to skip descendants already covered by an ancestor write. That only worked when the excluded ancestor happened to be a build root itself; a non-project directory containing multiple sibling projects would leave later siblings enabled. Instead, group excluded projects by the shallowest directory that actually matches --exclude-paths and write disabled:true there once, covering every ecosystem and sibling/nested project beneath it regardless of whether that directory is a build root of its own. * Abort recursive facts generation when a build root's workspace layout is unknown A build root whose facts generation fails (a build-tool crash or a blocking resolution failure) never produces its projects[] list, so there's no way to tell whether a later candidate underneath it is already covered by that root or a genuinely independent project. Continuing to process further candidates in that state risked misclassifying subprojects and piling on doomed attempts against a build already known to be broken. Fail closed instead: abort the entire recursive walk as soon as one build root's workspace layout can't be determined, rather than continuing to sibling and nested candidates. * Make recursive setup actually configure discovered build roots The recursive wizard (`socket manifest setup --dynamic-sbom-inference`) previously only ever disabled build roots matching --exclude-paths; every other discovered root was left completely untouched, with no way to set its bin/JDK/opts short of running the plain single-project wizard on it directly. Every non-excluded candidate now gets an interactive configure-or-inherit-defaults prompt (in discovery order, parent before child), seeded with its cascaded effective value so accepting every prompt unchanged preserves whatever it already inherits. Disabling a specific candidate is intentionally not offered here - that stays --exclude-paths' job, so a whole excluded subtree still collapses into a single write. Also fixes a real bug surfaced along the way: askForBin pre-filled the hardcoded tool fallback (mvn/./gradlew/sbt) as the prompt's shown value, so accepting the default was indistinguishable from explicitly typing it and got written to socket.json for no reason. The shown default is now only ever a prior explicit value; the fallback is mentioned as a hint instead. A matching guard drops any ecosystem section that ends up empty so it doesn't count as configured or trigger a write. Finally, the root step now detects which ecosystems are actually present at cwd (reusing the same check the plain wizard uses) and asks about detected ecosystems first, phrased accordingly, before offering to configure undetected ones "anyway" for subprojects that might need them. * Fix null-clear and exclude-paths bugs in the recursive setup wizard Fixes two correctness bugs found in an audit pass: leaving a config prompt blank when the field was already explicitly cleared (null) deleted the key instead of preserving the clear, silently reverting it to inheriting an ancestor's value; and --exclude-paths never reached the wizard's workspace enumeration, so excluding a broken reactor member didn't stop the wizard from still trying to resolve it and aborting the whole walk. Also tightens several UX rough edges in the recursive wizard: drops a redundant write confirmation inconsistent with the rest of the flow, fixes a tally line that double-counted re-enabled candidates and never reported disabled ones, and softens wording that overclaimed knowledge the wizard doesn't actually have yet (a build root at cwd, a fixed --exclude-paths prompt count). Trims the surrounding comments down to non-obvious rationale only, per repo comment-style guidelines. * Trim verbose comments across the dynamic-sbom-inference feature Cuts several multi-line comment blocks down to their non-obvious why, matching the repo's comment-style guidelines. * Address code review findings on the dynamic-sbom-inference PR - A root-disabled ecosystem was dropped from the build-tool scan entirely, so a nested socket.json could never re-enable it. Moved the strip-before- scan helper (previously wizard-only) to discover-manifest-roots.mts and applied it in generateRecursiveManifests too, letting the existing per-directory cascade check be the sole arbiter of skip vs. include. - runManifestFacts's progress line used logger.log (stdout), polluting --json output with non-JSON lines ahead of the payload. Switched to logger.info (stderr), matching every other status line in that function. - `socket manifest setup --dynamic-sbom-inference` forwarded --exclude-paths without validation, unlike every sibling manifest command. Added the same assertValidExcludePaths call. - Recursive generation inferred a build-root failure from whether process.exitCode changed during a call, which misclassifies a real failure as empty if the exit code was already non-zero beforehand. Gave runManifestFacts an explicit null (failure) vs. undefined (empty) return so callers don't have to infer it from global state. - Verbose error logging in enumerate-workspaces/run-manifest-facts string-coerced the caught error directly; switched to the existing getErrorMessageOr helper. * Refresh CLI banner snapshots after merging v1.x's version bump The merge brought package.json's version from 1.1.150-prerelease to a clean release version, so these inline snapshots no longer matched. * Use 1.1.153-prerelease pending the stuck 1.1.152 release 1.1.152 never actually published (still 404s on the registry, staged dist-tag is still 1.1.151), so bump to the next-version-hint convention used elsewhere in this repo's release history and refresh the CLI banner snapshots to match. * Simplify dynamic-sbom-inference's summary output Drop reactor-member "skippedCovered" lines from the per-line table - they're implied by their parent's line already showing up, and the aggregate count in the summary still reports them. Also drop the "across N build root(s)" total from the summary line: it counted every candidate directory visited, including reactor members that aren't independent build roots, which overstated how many actually exist. * Reduce the summary line to just the generated count Drop failed/skipped/empty from the tally: a failure aborts the whole walk immediately rather than accumulating (and is already reported via its own fail message), and the disabled/covered/empty buckets count candidate directories rather than independent build roots, so a total there is just as misleading as the "N build root(s)" figure already removed. * Trim verbose comment in output-manifest-dynamic-sbom-inference * Make the CLI-banner-version test normalization suffix-aware Root cause of the flaky snapshot mismatches on this PR: whether the CLI itself redacts its version banner (VITEST baked in at build time) or prints the real one and leaves redaction to this test helper depends on env propagation into the build step, not just the test run - so the same source can produce either "<redacted>" or the real "vX.Y.Z-prerelease" depending on how it was built. normalizeBanner's regex only stripped a bare "vX.Y.Z", leaving a trailing prerelease suffix dangling in one case but not the other. Broadened it to match a trailing prerelease/build suffix and to be idempotent on an already-redacted value, so both cases normalize identically. Refreshed the now-correct snapshots. * Fix realpath/boundary mismatch and a silent-zero-projects gap generateRecursiveManifests read candidate dirs realpath-resolved (findBuildToolCandidates already does this) but passed the raw cwd as the socket.json cascade boundary and as the anchor for re-anchoring --exclude-paths. Whenever cwd contains a symlink (macOS /tmp -> /private/tmp, several CI layouts), the boundary comparison never matched: the cascade walked all the way to the filesystem root instead of stopping at cwd, and --exclude-paths silently stopped reaching the build tool invocation. Resolve cwd once and use it consistently for both. Also realpath-resolve a project's subprojectDir before adding it to the covered set, in both generate-recursive-manifests.mts and setup-recursive-manifest-config.mts, so a symlinked reactor member is correctly recognized as covered instead of escaping and being reinvoked as an independent root. enumerateWorkspaces treated a clean exit with zero projects as success, but every real enumeration reports at least the build's own root project, so zero projects always means the task never ran (e.g. an extension jar built before the workspace-enumeration participant existed). Drop the exit-code condition so this is always a failure. * Address remaining non-blocking review findings - Hide `socket manifest dynamic-sbom-inference` - its name collides with the unrelated, root-only --dynamic-sbom-inference flag on scan create/reach (different semantics: this one is recursive per-root). Keep it internal until that naming collision is resolved. - --json --verbose emitted invalid JSON: the verbose debug preamble and run-manifest-facts' verbose resolution-detail logging both wrote to stdout ahead of the JSON payload. This is the only manifest command with --json, so the combination was newly reachable. Gated the preamble on !json and switched the detail logging to stderr. - Fixed two stale "socket manifest setup --recursive" comments left over from an earlier flag name. - renderTable no longer prints a bare blank line when there are no non-covered outcomes to show. * Make configureCandidate write a minimal diff instead of snapshotting the cascade Every field shown to the sub-wizard (including ones inherited from an ancestor, never touched by the user) was written verbatim into the candidate's own file, permanently pinning that value against future changes to the ancestor - converting inheritance into a one-time snapshot despite the field-level cascade being the point. Now diffs the final seed against an ancestor-only baseline (the cascade computed from dir's parent, excluding dir's own file) and only writes fields that actually differ, so an untouched field keeps inheriting live. * Add compat-matrix coverage for the workspace-enumeration producers socket-workspaces.init.gradle, socket-workspaces.plugin.scala, and CoanaWorkspacesLifecycleParticipant/SocketWorkspacesRecordsEngine had no automated coverage at all - the local compat matrix only exercised the facts scripts, so a registration or compile error in the workspaces siblings would surface only as silent degradation (the setup wizard's reactor-coverage pruning treating a real reactor as if it had no members). Adds a smoke-test-workspaces.sh per ecosystem, wired into the same per-version matrix run-compat.sh already runs for the facts scripts, asserting each variant emits exactly a meta record plus the expected project record(s) and nothing else (no node/root/file records, confirming no dependency resolution happens). Verified locally against the currently installed gradle/maven/sbt. * Scope the fail-closed abort to the failing ecosystem, not the whole walk Coverage (`covered`/`disabledRoots`) is tracked per ecosystem, so a gradle failure carries no information about maven or sbt's own classification - yet aborting all remaining ecosystems meant an unrelated one could be blocked from ever starting, with nothing in the output explaining why. Narrowed the abort to the failing ecosystem's own loop, and added an 'aborted' status for that ecosystem's still-untried candidates so they show up explicitly instead of being silently absent from the output. * expandEnvVarRefs: report every missing var and add a $$ escape Only the first missing variable was ever reported, so a value referencing two unset vars needed two runs to discover both. Also had no way to represent a literal $WORD - $$ now expands to a literal $, so whatever follows it is left untouched. Added a dedicated test file; this function had none. * Centralize --exclude-paths glob compilation into one TypeScript module The six JVM producers (gradle/maven/sbt × facts/workspaces) each carried their own NIO PathMatcher-based exclude-glob implementation, including a "zero-depth variant expansion" workaround for a semantics mismatch between NIO's `**` and the CLI's micromatch. Following the same pattern already used for --include-configs/--exclude-configs (PR #1404), the glob is now compiled to a portable regex pattern source once in exclude-paths-glob.mts, transported via the existing -D/-P property mechanism, and each producer just Pattern.compile()s what it receives. Gradle/sbt/maven all share java.util.regex, so one dialect covers every producer. Verified end-to-end against real gradle, maven, and sbt invocations (facts and workspaces variants) with an actual --exclude-paths value.
1 parent e199dbf commit 07bbf95

69 files changed

Lines changed: 5565 additions & 263 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.config/rollup.dist.config.mjs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,14 @@ async function copyManifestScripts() {
9898
path.join(srcDir, 'socket-facts.plugin.scala'),
9999
path.join(destDir, 'socket-facts.plugin.scala'),
100100
),
101+
fs.copyFile(
102+
path.join(srcDir, 'socket-workspaces.init.gradle'),
103+
path.join(destDir, 'socket-workspaces.init.gradle'),
104+
),
105+
fs.copyFile(
106+
path.join(srcDir, 'socket-workspaces.plugin.scala'),
107+
path.join(destDir, 'socket-workspaces.plugin.scala'),
108+
),
101109
])
102110
const jarPath = path.join(
103111
srcDir,

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "socket",
3-
"version": "1.1.152",
3+
"version": "1.1.153-prerelease",
44
"description": "CLI for Socket.dev",
55
"contentPolicy": {
66
"class": "dual-use"

src/commands/manifest/cmd-manifest-auto.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'
55

66
import { detectManifestActions } from './detect-manifest-actions.mts'
77
import { generateAutoManifest } from './generate_auto_manifest.mts'
8+
import { excludePathsFlag } from './manifest-flags.mts'
89
import constants from '../../constants.mts'
910
import { commonFlags } from '../../flags.mts'
1011
import { cmdFlagValueToArray } from '../../utils/cmd.mts'
@@ -14,7 +15,6 @@ import { meowOrExit } from '../../utils/meow-with-subcommands.mts'
1415
import { getFlagListOutput } from '../../utils/output-formatting.mts'
1516
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
1617
import { assertValidExcludePaths } from '../scan/exclude-paths.mts'
17-
import { excludePathsFlag } from '../scan/reachability-flags.mts'
1818

1919
import type {
2020
CliCommandConfig,

src/commands/manifest/cmd-manifest-auto.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('socket manifest auto', async () => {
2323
$ socket manifest auto [options] [CWD=.]
2424
2525
Options
26-
--exclude-paths List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (\`--cwd\` if set), not the reachability target: \`tests\` matches only \`<cwd>/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
26+
--exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched relative to CWD (\`--cwd\` if set): \`tests\` matches only \`<cwd>/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
2727
--verbose Enable debug output (only for auto itself; sub-steps need to have it pre-configured), may help when running into errors
2828
2929
Tries to figure out what language your target repo uses. If it finds a
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import path from 'node:path'
2+
3+
import { logger } from '@socketsecurity/registry/lib/logger'
4+
5+
import { handleManifestDynamicSbomInference } from './handle-manifest-dynamic-sbom-inference.mts'
6+
import { excludePathsFlag } from './manifest-flags.mts'
7+
import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts'
8+
import { commonFlags, outputFlags } from '../../flags.mts'
9+
import { checkCommandInput } from '../../utils/check-input.mts'
10+
import { cmdFlagValueToArray } from '../../utils/cmd.mts'
11+
import { getOutputKind } from '../../utils/get-output-kind.mts'
12+
import { meowOrExit } from '../../utils/meow-with-subcommands.mts'
13+
import { getFlagListOutput } from '../../utils/output-formatting.mts'
14+
import { assertValidExcludePaths } from '../scan/exclude-paths.mts'
15+
16+
import type {
17+
CliCommandConfig,
18+
CliCommandContext,
19+
} from '../../utils/meow-with-subcommands.mts'
20+
21+
const config: CliCommandConfig = {
22+
commandName: 'dynamic-sbom-inference',
23+
description:
24+
'Recursively discover gradle/sbt/maven build roots and generate a Socket facts SBOM for each',
25+
// Hidden: `--dynamic-sbom-inference` already names an unrelated, root-only
26+
// scan create/reach flag (see reachability-flags.mts). Keep this hidden
27+
// until the naming collision between the two is resolved.
28+
hidden: true,
29+
flags: {
30+
...commonFlags,
31+
...outputFlags,
32+
...excludePathsFlag,
33+
verbose: {
34+
type: 'boolean',
35+
default: false,
36+
description: 'Print debug messages',
37+
},
38+
},
39+
help: (command, config) => `
40+
Usage
41+
$ ${command} [options] [CWD=.]
42+
43+
Recursively walks CWD, discovers independent gradle, sbt, and maven build
44+
roots, and generates a Socket facts SBOM (.socket.facts.json) for each,
45+
skipping subproject/reactor-module directories a parent build root already
46+
covers. Unlike \`socket manifest auto\`, this looks beyond CWD itself.
47+
48+
Options
49+
${getFlagListOutput(config.flags)}
50+
51+
Examples
52+
53+
$ ${command}
54+
$ ${command} ./monorepo
55+
`,
56+
}
57+
58+
export const cmdManifestDynamicSbomInference = {
59+
description: config.description,
60+
hidden: config.hidden,
61+
run,
62+
}
63+
64+
async function run(
65+
argv: string[] | readonly string[],
66+
importMeta: ImportMeta,
67+
{ parentName }: CliCommandContext,
68+
): Promise<void> {
69+
const cli = meowOrExit({
70+
argv,
71+
config,
72+
importMeta,
73+
parentName,
74+
})
75+
76+
const {
77+
dryRun,
78+
json,
79+
markdown,
80+
verbose: verboseFlag,
81+
} = cli.flags as {
82+
dryRun: boolean
83+
json: boolean
84+
markdown: boolean
85+
verbose: boolean | undefined
86+
}
87+
const verbose = !!verboseFlag
88+
89+
let [cwd = '.'] = cli.input
90+
// Note: path.resolve vs .join:
91+
// If given path is absolute then cwd should not affect it.
92+
cwd = path.resolve(process.cwd(), cwd)
93+
94+
// This debug block prints to stdout; --json's payload does too, so skip it
95+
// here (unlike the other manifest commands, this one supports --json).
96+
if (verbose && !json) {
97+
logger.group('- ', parentName, config.commandName, ':')
98+
logger.group('- flags:', cli.flags)
99+
logger.groupEnd()
100+
logger.log('- target:', cwd)
101+
logger.groupEnd()
102+
}
103+
104+
const outputKind = getOutputKind(json, markdown)
105+
106+
const wasValidInput = checkCommandInput(
107+
outputKind,
108+
{
109+
nook: true,
110+
test: cli.input.length <= 1,
111+
message: 'Can only accept one DIR (make sure to escape spaces!)',
112+
fail: `received ${cli.input.length}`,
113+
},
114+
{
115+
nook: true,
116+
test: !json || !markdown,
117+
message: `The \`${FLAG_JSON}\` and \`${FLAG_MARKDOWN}\` flags can not be used at the same time`,
118+
fail: 'bad',
119+
},
120+
)
121+
if (!wasValidInput) {
122+
return
123+
}
124+
125+
const excludePaths = cmdFlagValueToArray(cli.flags['excludePaths'])
126+
assertValidExcludePaths(excludePaths)
127+
128+
if (dryRun) {
129+
logger.log(constants.DRY_RUN_BAILING_NOW)
130+
return
131+
}
132+
133+
await handleManifestDynamicSbomInference({
134+
cwd,
135+
excludePaths,
136+
outputKind,
137+
verbose,
138+
})
139+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { describe, expect } from 'vitest'
2+
3+
import constants, {
4+
FLAG_CONFIG,
5+
FLAG_DRY_RUN,
6+
FLAG_HELP,
7+
} from '../../../src/constants.mts'
8+
import { cmdit, spawnSocketCli, testPath } from '../../../test/utils.mts'
9+
10+
describe('socket manifest dynamic-sbom-inference', async () => {
11+
const { binCliPath } = constants
12+
13+
cmdit(
14+
['manifest', 'dynamic-sbom-inference', FLAG_HELP, FLAG_CONFIG, '{}'],
15+
`should support ${FLAG_HELP}`,
16+
async cmd => {
17+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd, {
18+
cwd: testPath,
19+
})
20+
expect(stdout).toMatchInlineSnapshot(`
21+
"Recursively discover gradle/sbt/maven build roots and generate a Socket facts SBOM for each
22+
23+
Usage
24+
$ socket manifest dynamic-sbom-inference [options] [CWD=.]
25+
26+
Recursively walks CWD, discovers independent gradle, sbt, and maven build
27+
roots, and generates a Socket facts SBOM (.socket.facts.json) for each,
28+
skipping subproject/reactor-module directories a parent build root already
29+
covers. Unlike \`socket manifest auto\`, this looks beyond CWD itself.
30+
31+
Options
32+
--exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched relative to CWD (\`--cwd\` if set): \`tests\` matches only \`<cwd>/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
33+
--json Output as JSON
34+
--markdown Output as Markdown
35+
--verbose Print debug messages
36+
37+
Examples
38+
39+
$ socket manifest dynamic-sbom-inference
40+
$ socket manifest dynamic-sbom-inference ./monorepo"
41+
`)
42+
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
43+
"
44+
_____ _ _ /---------------
45+
| __|___ ___| |_ ___| |_ | CLI: <redacted>
46+
|__ | * | _| '_| -_| _| | token: <redacted>, org: <redacted>
47+
|_____|___|___|_,_|___|_|.dev | Command: \`socket manifest dynamic-sbom-inference\`, cwd: <redacted>"
48+
`)
49+
50+
expect(code, 'explicit help should exit with code 0').toBe(0)
51+
expect(stderr, 'banner includes base command').toContain(
52+
'`socket manifest dynamic-sbom-inference`',
53+
)
54+
},
55+
)
56+
57+
cmdit(
58+
['manifest', 'dynamic-sbom-inference', FLAG_DRY_RUN, FLAG_CONFIG, '{}'],
59+
'should exit with dry-run message before touching disk',
60+
async cmd => {
61+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd, {
62+
cwd: testPath,
63+
})
64+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
65+
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
66+
"
67+
_____ _ _ /---------------
68+
| __|___ ___| |_ ___| |_ | CLI: <redacted>
69+
|__ | * | _| '_| -_| _| | token: <redacted>, org: <redacted>
70+
|_____|___|___|_,_|___|_|.dev | Command: \`socket manifest dynamic-sbom-inference\`, cwd: <redacted>"
71+
`)
72+
73+
expect(code, 'dry-run should exit with code 0 if input ok').toBe(0)
74+
},
75+
)
76+
})

src/commands/manifest/cmd-manifest-gradle.mts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'
55

66
import { convertGradleToFacts } from './convert-gradle-to-facts.mts'
77
import { convertGradleToMaven } from './convert_gradle_to_maven.mts'
8+
import { excludePathsFlag } from './manifest-flags.mts'
89
import { parseBuildToolOpts } from './parse-build-tool-opts.mts'
910
import { resolveBuildToolBin } from './scripts/build-tool.mts'
1011
import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
@@ -16,7 +17,6 @@ import { meowOrExit } from '../../utils/meow-with-subcommands.mts'
1617
import { getFlagListOutput } from '../../utils/output-formatting.mts'
1718
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
1819
import { assertValidExcludePaths } from '../scan/exclude-paths.mts'
19-
import { excludePathsFlag } from '../scan/reachability-flags.mts'
2020

2121
import type {
2222
CliCommandConfig,
@@ -206,8 +206,8 @@ async function run(
206206
}
207207
}
208208
if (includeConfigs === undefined) {
209-
if (sockJson.defaults?.manifest?.gradle?.includeConfigs !== undefined) {
210-
includeConfigs = sockJson.defaults?.manifest?.gradle?.includeConfigs
209+
if (sockJson.defaults?.manifest?.gradle?.includeConfigs) {
210+
includeConfigs = sockJson.defaults.manifest.gradle.includeConfigs
211211
logger.info(
212212
`Using default --include-configs from ${SOCKET_JSON}:`,
213213
includeConfigs,
@@ -217,8 +217,8 @@ async function run(
217217
}
218218
}
219219
if (excludeConfigs === undefined) {
220-
if (sockJson.defaults?.manifest?.gradle?.excludeConfigs !== undefined) {
221-
excludeConfigs = sockJson.defaults?.manifest?.gradle?.excludeConfigs
220+
if (sockJson.defaults?.manifest?.gradle?.excludeConfigs) {
221+
excludeConfigs = sockJson.defaults.manifest.gradle.excludeConfigs
222222
logger.info(
223223
`Using default --exclude-configs from ${SOCKET_JSON}:`,
224224
excludeConfigs,
@@ -276,10 +276,13 @@ async function run(
276276
return
277277
}
278278

279+
const javaHome = sockJson.defaults?.manifest?.gradle?.javaHome ?? undefined
280+
279281
if (verbose) {
280282
logger.group()
281283
logger.info('- cwd:', cwd)
282284
logger.info('- gradle bin:', bin)
285+
logger.info('- java home:', javaHome || '(inherited)')
283286
logger.groupEnd()
284287
}
285288

@@ -302,6 +305,7 @@ async function run(
302305
gradleOpts: parsedGradleOpts,
303306
ignoreUnresolved: Boolean(ignoreUnresolved),
304307
includeConfigs: String(includeConfigs || ''),
308+
javaHome,
305309
verbose: Boolean(verbose),
306310
})
307311
return

src/commands/manifest/cmd-manifest-gradle.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ describe('socket manifest gradle', async () => {
2525
Options
2626
--bin Location of the gradle binary to use, default: ./gradlew if present, else gradle on PATH
2727
--exclude-configs When generating facts: comma-separated glob patterns; Gradle configurations matching any pattern are skipped (applied after --include-configs)
28-
--exclude-paths List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (\`--cwd\` if set), not the reachability target: \`tests\` matches only \`<cwd>/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
28+
--exclude-paths List of glob patterns to exclude from manifest/facts generation. Patterns are anchored micromatch globs matched relative to CWD (\`--cwd\` if set): \`tests\` matches only \`<cwd>/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
2929
--facts Emit a Socket facts JSON file (\`.socket.facts.json\`) describing the resolved dependency graph. This is the default; pass \`--pom\` to generate \`pom.xml\` files instead
3030
--gradle-opts Additional options to pass on to ./gradlew, see \`./gradlew --help\`
3131
--ignore-unresolved When generating facts: warn on unresolved dependencies instead of failing the run (unresolved deps are not emitted to the facts file)

src/commands/manifest/cmd-manifest-kotlin.mts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { logger } from '@socketsecurity/registry/lib/logger'
55

66
import { convertGradleToFacts } from './convert-gradle-to-facts.mts'
77
import { convertGradleToMaven } from './convert_gradle_to_maven.mts'
8+
import { excludePathsFlag } from './manifest-flags.mts'
89
import { parseBuildToolOpts } from './parse-build-tool-opts.mts'
910
import { resolveBuildToolBin } from './scripts/build-tool.mts'
1011
import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
@@ -16,7 +17,6 @@ import { meowOrExit } from '../../utils/meow-with-subcommands.mts'
1617
import { getFlagListOutput } from '../../utils/output-formatting.mts'
1718
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
1819
import { assertValidExcludePaths } from '../scan/exclude-paths.mts'
19-
import { excludePathsFlag } from '../scan/reachability-flags.mts'
2020

2121
import type {
2222
CliCommandConfig,
@@ -211,8 +211,8 @@ async function run(
211211
}
212212
}
213213
if (includeConfigs === undefined) {
214-
if (sockJson.defaults?.manifest?.gradle?.includeConfigs !== undefined) {
215-
includeConfigs = sockJson.defaults?.manifest?.gradle?.includeConfigs
214+
if (sockJson.defaults?.manifest?.gradle?.includeConfigs) {
215+
includeConfigs = sockJson.defaults.manifest.gradle.includeConfigs
216216
logger.info(
217217
`Using default --include-configs from ${SOCKET_JSON}:`,
218218
includeConfigs,
@@ -222,8 +222,8 @@ async function run(
222222
}
223223
}
224224
if (excludeConfigs === undefined) {
225-
if (sockJson.defaults?.manifest?.gradle?.excludeConfigs !== undefined) {
226-
excludeConfigs = sockJson.defaults?.manifest?.gradle?.excludeConfigs
225+
if (sockJson.defaults?.manifest?.gradle?.excludeConfigs) {
226+
excludeConfigs = sockJson.defaults.manifest.gradle.excludeConfigs
227227
logger.info(
228228
`Using default --exclude-configs from ${SOCKET_JSON}:`,
229229
excludeConfigs,
@@ -279,10 +279,13 @@ async function run(
279279
return
280280
}
281281

282+
const javaHome = sockJson.defaults?.manifest?.gradle?.javaHome ?? undefined
283+
282284
if (verbose) {
283285
logger.group()
284286
logger.info('- cwd:', cwd)
285287
logger.info('- gradle bin:', bin)
288+
logger.info('- java home:', javaHome || '(inherited)')
286289
logger.groupEnd()
287290
}
288291

@@ -305,6 +308,7 @@ async function run(
305308
gradleOpts: parsedGradleOpts,
306309
ignoreUnresolved: Boolean(ignoreUnresolved),
307310
includeConfigs: String(includeConfigs || ''),
311+
javaHome,
308312
verbose: Boolean(verbose),
309313
})
310314
return

0 commit comments

Comments
 (0)