@socketsecurity/cli 1.2.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.4.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.4.0) - 2026-09-29
8
+
9
+ ### Changed
10
+ - Updated the Coana CLI to v `15.11.4`.
11
+
12
+ ## [1.3.0](https://github.com/SocketDev/socket-cli/releases/tag/v1.3.0) - 2026-09-29
13
+
14
+ ### Added
15
+
16
+ - `socket scan create --uv-members <dir...>` scans selected uv workspace members with the versions and dependency relationships pinned in their shared `uv.lock`, leaving unrelated members out of the scan. Dependencies used only by dependency groups are marked as development dependencies. Requires uv with CycloneDX export support.
17
+
7
18
  ## [1.2.1](https://github.com/SocketDev/socket-cli/releases/tag/v1.2.1) - 2026-09-28
8
19
 
9
20
  ### Changed
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Socket CLI
2
2
 
3
3
  [![Socket Badge](https://socket.dev/api/badge/npm/package/socket)](https://socket.dev/npm/package/socket)
4
- [![Follow @SocketSecurity](https://raw.githubusercontent.com/SocketDev/socket-cli/v1.2.1/assets/fleet/badge-follow-x.svg)](https://twitter.com/SocketSecurity)
5
- [![Follow @socket.dev on Bluesky](https://raw.githubusercontent.com/SocketDev/socket-cli/v1.2.1/assets/fleet/badge-follow-bluesky.svg)](https://bsky.app/profile/socket.dev)
4
+ [![Follow @SocketSecurity](https://raw.githubusercontent.com/SocketDev/socket-cli/v1.4.0/assets/fleet/badge-follow-x.svg)](https://twitter.com/SocketSecurity)
5
+ [![Follow @socket.dev on Bluesky](https://raw.githubusercontent.com/SocketDev/socket-cli/v1.4.0/assets/fleet/badge-follow-bluesky.svg)](https://bsky.app/profile/socket.dev)
6
6
 
7
7
  CLI for [Socket.dev](https://socket.dev) — scan dependencies, audit packages, apply security patches, and gate installs from your terminal or CI. This is the 1.x line of the published `socket` package on npm.
8
8
 
@@ -63,6 +63,39 @@ All aliases support the flags and arguments of the commands they alias.
63
63
 
64
64
  - `socket ci` - Alias for `socket scan create --report` (creates report and exits with error if unhealthy)
65
65
 
66
+ ### Scanning uv workspace members
67
+
68
+ Use `--uv-members` to scan selected directories of a uv workspace with the
69
+ versions pinned in the workspace's shared `uv.lock`:
70
+
71
+ ```sh
72
+ socket scan create --uv-members ./packages/api ./packages/worker
73
+ ```
74
+
75
+ Each TARGET is a directory with its own `pyproject.toml`. uv finds the
76
+ `uv.lock` in that directory or in its workspace root, so one scan can cover
77
+ members of several workspaces. The scan includes each target's transitive and
78
+ local workspace dependencies, all extras, and all dependency groups.
79
+ Dependencies used only by dependency groups are marked as development
80
+ dependencies. Unrelated members and the shared lockfile are not added to the
81
+ scan.
82
+
83
+ This mode requires uv on PATH with support for `uv export --format cyclonedx1.5`,
84
+ which uv currently treats as a preview feature. The export runs offline with
85
+ `--frozen`, so it uses the existing lockfile without resolving newer versions,
86
+ installing packages, or changing the project.
87
+
88
+ The CLI writes a `socket-uv-cdx.json` SBOM into each target directory, uploads
89
+ only those SBOMs in place of regular manifest discovery, and removes them after
90
+ the scan, including on failure, Ctrl-C, or SIGTERM. It stops if that file
91
+ already exists.
92
+ `--read-only` prepares the SBOMs without uploading them, and `--dry-run`
93
+ validates the options without running uv.
94
+
95
+ With `--reach`, pass a single target. Reachability analysis then runs on that
96
+ member's directory. `--uv-members` cannot be combined with `--auto-manifest` or
97
+ `--dynamic-sbom-inference`.
98
+
66
99
  ### Reachability analysis
67
100
 
68
101
  Socket reachability analysis comes in three forms:
@@ -138,8 +171,8 @@ MIT
138
171
  <br/>
139
172
  <div align="center">
140
173
  <picture>
141
- <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/SocketDev/socket-cli/v1.2.1/assets/fleet/socket-combomark-dark.svg">
142
- <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/SocketDev/socket-cli/v1.2.1/assets/fleet/socket-combomark-light.svg">
143
- <img width="324" height="104" alt="Socket" src="https://raw.githubusercontent.com/SocketDev/socket-cli/v1.2.1/assets/fleet/socket-combomark-light.svg">
174
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/SocketDev/socket-cli/v1.4.0/assets/fleet/socket-combomark-dark.svg">
175
+ <source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/SocketDev/socket-cli/v1.4.0/assets/fleet/socket-combomark-light.svg">
176
+ <img width="324" height="104" alt="Socket" src="https://raw.githubusercontent.com/SocketDev/socket-cli/v1.4.0/assets/fleet/socket-combomark-light.svg">
144
177
  </picture>
145
178
  </div>
package/dist/cli.js CHANGED
@@ -2693,7 +2693,11 @@ function assembleFacts(parsed, opts = {}) {
2693
2693
  finalNodes
2694
2694
  } = mergeByCoordinate(perRoot);
2695
2695
  const tool = parsed.tool || 'gradle';
2696
- const components = buildComponents(finalNodes);
2696
+ const projectsByGav = new Map();
2697
+ for (const p of parsed.projects.values()) {
2698
+ projectsByGav.set(gav(p.group, p.name, p.version), p);
2699
+ }
2700
+ const components = buildComponents(finalNodes, projectsByGav);
2697
2701
  const projects = opts.emitProjects === false ? [] : buildProjects(parsed, directByRoot, perRoot);
2698
2702
  const metadata = {
2699
2703
  format: 'socket-facts-sbom',
@@ -2714,7 +2718,7 @@ function assembleFacts(parsed, opts = {}) {
2714
2718
  return {
2715
2719
  facts,
2716
2720
  report: buildReport(parsed),
2717
- artifactPaths: buildArtifactPaths(finalNodes, [...parsed.projects.values()], perRoot, fileExists)
2721
+ artifactPaths: buildArtifactPaths(finalNodes, [...parsed.projects.values()], projectsByGav, perRoot, fileExists)
2718
2722
  };
2719
2723
  }
2720
2724
  function gav(group, name, version) {
@@ -2802,7 +2806,7 @@ function mergeByCoordinate(perRoot) {
2802
2806
  directByRoot
2803
2807
  };
2804
2808
  }
2805
- function buildComponents(finalNodes) {
2809
+ function buildComponents(finalNodes, projectsByGav) {
2806
2810
  return [...finalNodes.keys()].sort().map(id => {
2807
2811
  const fn = finalNodes.get(id);
2808
2812
  const c = fn.coord;
@@ -2833,6 +2837,9 @@ function buildComponents(finalNodes) {
2833
2837
  if (!fn.prod) {
2834
2838
  comp.dev = true;
2835
2839
  }
2840
+ if (projectsByGav.has(gav(c.group, c.name, c.version ?? ''))) {
2841
+ comp.firstParty = true;
2842
+ }
2836
2843
  if (fn.children.size) {
2837
2844
  comp.dependencies = [...fn.children].sort();
2838
2845
  }
@@ -2923,14 +2930,7 @@ function buildClasspathByProject(projects, perRoot) {
2923
2930
  1: ids
2924
2931
  }) => [key, [...ids].sort()]));
2925
2932
  }
2926
- function buildArtifactPaths(finalNodes, projects, perRoot, fileExists) {
2927
- const projectsByGav = new Map();
2928
- for (const p of projects) {
2929
- projectsByGav.set(gav(p.group, p.name, p.version), {
2930
- sources: p.sources,
2931
- targets: p.targets
2932
- });
2933
- }
2933
+ function buildArtifactPaths(finalNodes, projects, projectsByGav, perRoot, fileExists) {
2934
2934
  const targetsByCoord = new Map();
2935
2935
  const targetsByGav = new Map();
2936
2936
  const sourcesByCoord = new Map();
@@ -7116,7 +7116,7 @@ function filterToPregeneratedSboms(filepaths, supportedFiles) {
7116
7116
  nocase: true
7117
7117
  }));
7118
7118
  }
7119
- async function handleCreateNewScan({
7119
+ async function createNewScan({
7120
7120
  autoManifest,
7121
7121
  branchName,
7122
7122
  commitHash,
@@ -7137,7 +7137,7 @@ async function handleCreateNewScan({
7137
7137
  targets,
7138
7138
  tmp,
7139
7139
  workspace
7140
- }) {
7140
+ }, scanFiles) {
7141
7141
  let scanTargets = targets;
7142
7142
  debug.debugFn('notice', `Creating new scan for ${orgSlug}/${workspace ? `${workspace}/` : ''}${repoName}`);
7143
7143
  debug.debugDir('inspect', {
@@ -7251,11 +7251,11 @@ async function handleCreateNewScan({
7251
7251
  reachabilityOptions: reach,
7252
7252
  target: targets[0]
7253
7253
  });
7254
- const packagePaths = await utils.getPackageFilesForScan(scanTargets, supportedFiles, {
7254
+ const packagePaths = scanFiles ?? (await utils.getPackageFilesForScan(scanTargets, supportedFiles, {
7255
7255
  additionalIgnores: additionalScaIgnores,
7256
7256
  config: socketConfig,
7257
7257
  cwd
7258
- });
7258
+ }));
7259
7259
  spinner.successAndStop(`Found ${packagePaths.length} ${words.pluralize('file', packagePaths.length)} to include in scan.`);
7260
7260
  const wasValidInput = utils.checkCommandInput(outputKind, {
7261
7261
  nook: true,
@@ -7436,6 +7436,14 @@ async function handleCreateNewScan({
7436
7436
  }
7437
7437
  });
7438
7438
  }
7439
+ async function handleCreateNewScan(config) {
7440
+ const generated = await config.generateScanFiles?.();
7441
+ try {
7442
+ await createNewScan(config, generated?.files);
7443
+ } finally {
7444
+ await generated?.cleanup();
7445
+ }
7446
+ }
7439
7447
 
7440
7448
  /**
7441
7449
  * Derive the pull request number from the CI environment. GitHub Actions
@@ -19575,6 +19583,115 @@ const cmdRepository = {
19575
19583
  }
19576
19584
  };
19577
19585
 
19586
+ const CLEANUP_SIGNALS = ['SIGHUP', 'SIGINT', 'SIGTERM'];
19587
+ const UV_SBOM_FILENAME = 'socket-uv-cdx.json';
19588
+ function componentIdentity(component) {
19589
+ return JSON.stringify([component.name, component.version, component.purl]);
19590
+ }
19591
+ async function exportMemberSbom(memberDir) {
19592
+ const sbom = await exportSbom(memberDir, '--all-groups');
19593
+ const productionSbom = await exportSbom(memberDir, '--no-default-groups');
19594
+ // uv can renumber bom-ref values between exports.
19595
+ const productionPackages = new Set(productionSbom.components?.map(componentIdentity));
19596
+ for (const component of sbom.components ?? []) {
19597
+ component.scope = productionPackages.has(componentIdentity(component)) ? 'required' : 'optional';
19598
+ }
19599
+ return sbom;
19600
+ }
19601
+ async function exportSbom(memberDir, groups) {
19602
+ let content;
19603
+ try {
19604
+ const {
19605
+ stdout
19606
+ } = await spawn.spawn('uv', ['export', '--project', memberDir, '--format', 'cyclonedx1.5', '--frozen', '--offline', '--no-python-downloads', '--all-extras', groups], {
19607
+ cwd: memberDir,
19608
+ signal: constants.default.abortSignal,
19609
+ stdio: 'pipe'
19610
+ });
19611
+ content = stdout;
19612
+ } catch (e) {
19613
+ throw new utils.InputError(`Could not export the uv dependency graph for ${memberDir}. Install uv on PATH with CycloneDX export support and check that the directory is a uv project with a uv.lock in it or in its workspace root.`, e && typeof e === 'object' && 'stderr' in e ? String(e.stderr).trim() || utils.getErrorMessage(e) : utils.getErrorMessage(e));
19614
+ }
19615
+ return parseSbom(content, memberDir);
19616
+ }
19617
+ function parseSbom(content, memberDir) {
19618
+ try {
19619
+ const sbom = JSON.parse(content);
19620
+ if (sbom?.bomFormat === 'CycloneDX' && Array.isArray(sbom.dependencies) && (sbom.components === undefined || Array.isArray(sbom.components)) && typeof sbom.metadata?.component?.name === 'string') {
19621
+ return sbom;
19622
+ }
19623
+ } catch {}
19624
+ throw new utils.InputError(`uv did not return a CycloneDX dependency graph for ${memberDir}. Update uv and try again.`);
19625
+ }
19626
+ async function generateUvMemberSboms(memberDirs) {
19627
+ const sboms = [];
19628
+ for (const memberDir of memberDirs) {
19629
+ logger.logger.info(`Exporting the uv dependency graph for ${memberDir}...`);
19630
+ // eslint-disable-next-line no-await-in-loop
19631
+ sboms.push(await exportMemberSbom(memberDir));
19632
+ }
19633
+ const files = [];
19634
+ const removeFilesSync = () => {
19635
+ for (const file of files) {
19636
+ fs$1.rmSync(file, {
19637
+ force: true
19638
+ });
19639
+ }
19640
+ };
19641
+ // A signalled or exiting scan skips finally blocks, and the bin launcher
19642
+ // SIGKILLs a signalled scan after a short grace period.
19643
+ process.once('exit', removeFilesSync);
19644
+ for (const signal of CLEANUP_SIGNALS) {
19645
+ process.once(signal, removeFilesSync);
19646
+ }
19647
+ const cleanup = async () => {
19648
+ process.removeListener('exit', removeFilesSync);
19649
+ for (const signal of CLEANUP_SIGNALS) {
19650
+ process.removeListener(signal, removeFilesSync);
19651
+ }
19652
+ await Promise.all(files.map(file => fs$1.promises.rm(file, {
19653
+ force: true
19654
+ })));
19655
+ };
19656
+ try {
19657
+ for (let i = 0; i < memberDirs.length; i += 1) {
19658
+ // The SBOM sits where the member's own manifest would, so upload paths
19659
+ // and the reachability target line up with the member directory.
19660
+ const filename = path.join(memberDirs[i], UV_SBOM_FILENAME);
19661
+ // eslint-disable-next-line no-await-in-loop
19662
+ await fs$1.promises.writeFile(filename, JSON.stringify(sboms[i]), {
19663
+ flag: 'wx'
19664
+ });
19665
+ files.push(filename);
19666
+ }
19667
+ } catch (e) {
19668
+ await cleanup();
19669
+ throw e;
19670
+ }
19671
+ return {
19672
+ cleanup,
19673
+ files
19674
+ };
19675
+ }
19676
+ function resolveUvMemberDirs(targets, cwd) {
19677
+ const memberDirs = new Set();
19678
+ for (const target of targets) {
19679
+ const memberDir = path.resolve(cwd, target);
19680
+ const relativeDir = path.relative(cwd, memberDir);
19681
+ if (relativeDir === '..' || relativeDir.startsWith(`..${path.sep}`) || path.isAbsolute(relativeDir) || !fs$2.isDirSync(memberDir)) {
19682
+ throw new utils.InputError(`--uv-members requires every TARGET to be a directory inside --cwd, but got ${target}`);
19683
+ }
19684
+ if (!fs$1.existsSync(path.join(memberDir, 'pyproject.toml'))) {
19685
+ throw new utils.InputError(`--uv-members requires a pyproject.toml in every TARGET, but ${target} has none`);
19686
+ }
19687
+ if (fs$1.existsSync(path.join(memberDir, UV_SBOM_FILENAME))) {
19688
+ throw new utils.InputError(`${path.join(target, UV_SBOM_FILENAME)} already exists. Remove it and run the scan again.`);
19689
+ }
19690
+ memberDirs.add(memberDir);
19691
+ }
19692
+ return Array.from(memberDirs);
19693
+ }
19694
+
19578
19695
  async function suggestTarget() {
19579
19696
  // We could prefill this with sub-dirs of the current
19580
19697
  // dir ... but is that going to be useful?
@@ -19737,6 +19854,11 @@ const generalFlags$1 = {
19737
19854
  default: false,
19738
19855
  description: 'Set the visibility (true/false) of the scan in your dashboard.',
19739
19856
  shortFlag: 't'
19857
+ },
19858
+ uvMembers: {
19859
+ type: 'boolean',
19860
+ default: false,
19861
+ description: 'Scan each TARGET directory as a uv project, using the versions pinned in its uv.lock or its workspace root uv.lock. Uploads a CycloneDX dependency graph per TARGET in place of manifest discovery, including all extras and dependency groups. Requires uv on PATH.'
19740
19862
  }
19741
19863
  };
19742
19864
  const cmdScanCreate = {
@@ -19806,6 +19928,7 @@ async function run$d(argv, importMeta, {
19806
19928
  $ ${command}
19807
19929
  $ ${command} ./proj --json
19808
19930
  $ ${command} --repo=test-repo --branch=main ./package.json
19931
+ $ ${command} --uv-members ./packages/api ./packages/worker
19809
19932
  `
19810
19933
  };
19811
19934
  const cli = utils.meowOrExit({
@@ -19855,6 +19978,7 @@ async function run$d(argv, importMeta, {
19855
19978
  // Validate ecosystem values against the reachability-supported set.
19856
19979
  const reachEcosystems = utils.parseReachEcosystems(utils.cmdFlagValueToArray(cli.flags['reachEcosystems']));
19857
19980
  const dryRun = !!cli.flags['dryRun'];
19981
+ const uvMembers = !!cli.flags['uvMembers'];
19858
19982
  let {
19859
19983
  autoManifest,
19860
19984
  branch: branchName,
@@ -19964,7 +20088,7 @@ async function run$d(argv, importMeta, {
19964
20088
  // to regenerate it would be misleading; the manifest data is already there
19965
20089
  // and will be picked up by the scan.
19966
20090
  const hasFactsFile = fs$1.existsSync(path.join(cwd, constants.default.DOT_SOCKET_DOT_FACTS_JSON));
19967
- if (detected.count > 0 && !autoManifest && !dynamicSbomInference && !hasFactsFile) {
20091
+ if (detected.count > 0 && !autoManifest && !dynamicSbomInference && !uvMembers && !hasFactsFile) {
19968
20092
  logger.logger.info(`Detected ${detected.count} manifest targets we could try to generate. Please set the --auto-manifest flag if you want to include languages covered by \`socket manifest auto\` in the Scan.`);
19969
20093
  }
19970
20094
  if (updatedInput && orgSlug && targets.length) {
@@ -20016,6 +20140,11 @@ async function run$d(argv, importMeta, {
20016
20140
  test: !!targets.length,
20017
20141
  message: 'At least one TARGET (e.g. `.` or `./package.json`)',
20018
20142
  fail: 'missing'
20143
+ }, {
20144
+ nook: true,
20145
+ test: !uvMembers || !autoManifest && !dynamicSbomInference,
20146
+ message: '--uv-members cannot be combined with --auto-manifest or --dynamic-sbom-inference',
20147
+ fail: 'select one source of generated SBOMs'
20019
20148
  }, {
20020
20149
  nook: true,
20021
20150
  test: !json || !markdown,
@@ -20065,6 +20194,7 @@ async function run$d(argv, importMeta, {
20065
20194
  if (!wasValidInput) {
20066
20195
  return;
20067
20196
  }
20197
+ const uvMemberDirs = uvMembers ? resolveUvMemberDirs(targets, cwd) : undefined;
20068
20198
  if (dryRun) {
20069
20199
  logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
20070
20200
  return;
@@ -20077,6 +20207,7 @@ async function run$d(argv, importMeta, {
20077
20207
  committers: committers && String(committers) || '',
20078
20208
  cwd,
20079
20209
  defaultBranch: Boolean(defaultBranch),
20210
+ generateScanFiles: uvMemberDirs ? () => generateUvMemberSboms(uvMemberDirs) : undefined,
20080
20211
  interactive: Boolean(interactive),
20081
20212
  orgSlug,
20082
20213
  outputKind,
@@ -24275,5 +24406,5 @@ process.on('unhandledRejection', async (reason, promise) => {
24275
24406
  // eslint-disable-next-line n/no-process-exit
24276
24407
  process.exit(1);
24277
24408
  });
24278
- //# debugId=45e6f932-d7fe-49d0-91f3-6a45a49eecd3
24409
+ //# debugId=df6cd218-d465-449e-b81a-890e662d0baf
24279
24410
  //# sourceMappingURL=cli.js.map