Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [main, v1.x]
pull_request:
branches: [main, v1.x]
branches: [main, v1.x, docs/organization-dependencies-v1x]

permissions:
contents: read
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ Supports version 2 format with `projectIgnorePaths` for excluding files from rep

### See also

- [Organization dependency inventory guide](docs/organization-dependencies.md)

- [Socket API Reference](https://docs.socket.dev/reference)
- [Socket GitHub App](https://github.com/apps/socket-security)
- [`@socketsecurity/sdk`](https://github.com/SocketDev/socket-sdk-js)
Expand Down
93 changes: 93 additions & 0 deletions docs/organization-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Organization dependency inventory

`socket organization dependencies` lists dependency records already known to
the authenticated organization. It does not scan the current directory. This
command does not accept a package name or an organization slug.

## Access

Run `socket login` to save a Socket API token, or set the supported
`SOCKET_CLI_API_TOKEN` environment variable. The inventory available to the
command depends on the credential you use. The command requires API access.

## List dependencies

Run the command without options to request the first 50 dependency artifacts:

```sh
socket organization dependencies
```

Use `--limit` to set the number of dependency artifacts requested. The API
accepts values from 1 to 100. The default is 50. Use `--offset` to skip that
many dependency artifacts; the default is 0.
For example, request results in batches of 20:

```sh
socket organization dependencies --limit 20 --offset 0
socket organization dependencies --limit 20 --offset 20
socket organization dependencies --limit 20 --offset 40
```

Check the output's “Is there more data after this?” value. Continue with the
next offset until it says `no`. The limit applies to dependency artifacts. An
artifact can have more than one repository link, so the displayed row count
can be greater than the limit. Each invocation makes one request; the command
does not paginate automatically.

## Filter by package

Use `--purls` to find dependency artifacts by package URL (PURL). Supply one
complete PURL per flag; repeat the flag to search for multiple packages:

```sh
socket organization dependencies --purls 'pkg:npm/minimist@0.0.4'
socket organization dependencies \
--purls 'pkg:npm/minimist' \
--purls 'pkg:pypi/requests@2.32.3' \
--json
```

A PURL can include an exact version or omit the version to match all indexed
versions of that package. Package type, namespace, and name are matched as
provided; for example, `pkg:npm/%40types/node` keeps the scoped npm namespace.
Searches do not support substring, wildcard, or version-range matching.
Qualifiers and subpaths are rejected because this search endpoint filters only
package identity and version. If the server does not confirm every requested
PURL, the CLI reports a failure and does not display dependency rows.

Each request searches one page, so combine `--purls` with `--limit` and
`--offset` to continue through matching artifacts. The rows include the
organization repositories and branches currently linked to those artifacts.

## Choose an output format

The default output and `--markdown` display a text table with ecosystem,
namespace, name, version, repository, branch, and direct dependency columns.
The output also shows the requested offset and limit and whether more data is
available.

Use `--json` to write the result envelope to a file:

```sh
socket organization dependencies --json > dependencies.json
```

On success, the JSON envelope contains `ok` and `data`. Read dependency rows
from `data.rows`; `data` also includes `end`, `limit`, and `offset`. When you
search with `--purls`, it also includes the server's filter confirmation in
`data.purlFilters`. The `--json` and `--markdown` options cannot be used
together.

## Help and dry run

Use `--help` to see the available options:

```sh
socket organization dependencies --help
```

A dry run does not retrieve inventory. It only reports what the command would
request. On the 2.x prerelease, dry run requires authentication and reports the
selected limit, offset, and PURL filters. On 1.x, it exits before making a
request and prints a generic dry-run message.
16 changes: 15 additions & 1 deletion src/commands/organization/cmd-organization-dependencies.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import { handleDependencies } from './handle-dependencies.mts'
import constants, { FLAG_JSON, FLAG_MARKDOWN } from '../../constants.mts'
import { commonFlags, outputFlags } from '../../flags.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { InputError } from '../../utils/errors.mts'
import { getOutputKind } from '../../utils/get-output-kind.mts'
import { meowOrExit } from '../../utils/meow-with-subcommands.mts'
import {
getFlagApiRequirementsOutput,
getFlagListOutput,
} from '../../utils/output-formatting.mts'
import { hasDefaultApiToken } from '../../utils/sdk.mts'
import { parsePurlsOption } from './dependencies-filters.mts'

import type {
CliCommandConfig,
Expand Down Expand Up @@ -49,7 +51,12 @@ async function run(
offset: {
type: 'number',
default: 0,
description: 'Page number',
description: 'Number of dependencies to skip',
},
purls: {
type: 'string',
isMultiple: true,
description: 'Search for a full PURL; repeat this flag for more PURLs',
},
...outputFlags,
},
Expand All @@ -66,6 +73,7 @@ async function run(
Examples
${command}
${command} --limit 20 --offset 10
${command} --purls 'pkg:npm/minimist@0.0.4'
`,
}

Expand All @@ -77,6 +85,11 @@ async function run(
})

const { json, limit, markdown, offset } = cli.flags
const parsedPurls = parsePurlsOption(argv, cli.flags['purls'])
if (!parsedPurls.ok) {
throw new InputError(parsedPurls.message)
}
const { purls } = parsedPurls

const dryRun = !!cli.flags['dryRun']

Expand Down Expand Up @@ -111,6 +124,7 @@ async function run(
await handleDependencies({
limit: Number(limit || 0) || 0,
offset: Number(offset || 0) || 0,
purls,
outputKind,
})
}
79 changes: 77 additions & 2 deletions src/commands/organization/cmd-organization-dependencies.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ describe('socket organization dependencies', async () => {
--json Output as JSON
--limit Maximum number of dependencies returned
--markdown Output as Markdown
--offset Page number
--offset Number of dependencies to skip
--purls Search for a full PURL; repeat this flag for more PURLs

Examples
socket organization dependencies
socket organization dependencies --limit 20 --offset 10"
socket organization dependencies --limit 20 --offset 10
socket organization dependencies --purls 'pkg:npm/minimist@0.0.4'"
`,
)
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -74,4 +76,77 @@ describe('socket organization dependencies', async () => {
expect(code, 'dry-run should exit with code 0 if input ok').toBe(0)
},
)

cmdit(
[
'organization',
'dependencies',
FLAG_DRY_RUN,
'--purls',
'pkg:npm/minimist@0.0.4',
'--purls',
'pkg:pypi/requests',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should accept repeated PURL filters in a dry run',
async cmd => {
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
expect(code).toBe(0)
expect(stdout).toContain('[DryRun]: Bailing now')
},
)

cmdit(
[
'organization',
'dependencies',
FLAG_DRY_RUN,
'--purls',
'minimist',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should reject invalid PURLs before dry-run success',
async cmd => {
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
expect(code).not.toBe(0)
expect(`${stdout}\n${stderr}`).toContain('Invalid PURL "minimist"')
expect(stdout).not.toContain('[DryRun]: Bailing now')
},
)

cmdit(
[
'organization',
'dependencies',
FLAG_DRY_RUN,
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
'--purls',
],
'should reject a bare PURL flag',
async cmd => {
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
expect(code).not.toBe(0)
expect(stdout).not.toContain('[DryRun]: Bailing now')
},
)

cmdit(
[
'organization',
'dependencies',
FLAG_DRY_RUN,
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
'--purls=',
],
'should reject an explicitly empty PURL filter',
async cmd => {
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
expect(code).not.toBe(0)
expect(stdout).not.toContain('[DryRun]: Bailing now')
},
)
})
115 changes: 115 additions & 0 deletions src/commands/organization/dependencies-filters.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import { getPurlObject } from '../../utils/purl.mts'

export type DependenciesPurlsResult =
| { ok: true; purls: string[] | undefined }
| { ok: false; message: string }

export function parsePurlsOption(
argv: readonly string[],
suppliedPurls: unknown,
): DependenciesPurlsResult {
const hasPurlsOption = argv.some(
argument => argument === '--purls' || argument.startsWith('--purls='),
)
if (suppliedPurls === undefined) {
return hasPurlsOption
? parseDependenciesPurls([])
: { ok: true, purls: undefined }
}
if (
!hasPurlsOption &&
Array.isArray(suppliedPurls) &&
suppliedPurls.length === 0
) {
return { ok: true, purls: undefined }
}
if (
!Array.isArray(suppliedPurls) ||
!suppliedPurls.every(
(value: unknown): value is string => typeof value === 'string',
)
) {
return {
ok: false,
message: 'Provide at least one non-empty PURL with --purls.',
}
}
return parseDependenciesPurls(suppliedPurls)
}

export function parseDependenciesPurls(
values: readonly string[] | undefined,
): DependenciesPurlsResult {
if (values === undefined) {
return { ok: true, purls: undefined }
}

const purls: string[] = []
const seen = new Set<string>()

for (const value of values) {
const purl = value.trim()
const parsed =
purl.startsWith('pkg:') && purl.length > 4
? getPurlObject(purl, { throws: false })
: undefined

if (!parsed) {
return {
ok: false,
message: `Invalid PURL "${value}". Use a full PURL such as pkg:npm/minimist@1.2.3.`,
}
}

if (Object.keys(parsed.qualifiers ?? {}).length > 0 || parsed.subpath) {
return {
ok: false,
message: `PURL "${value}" includes qualifiers or a subpath, which this search does not support. Search by package identity and version only.`,
}
}

if (!seen.has(purl)) {
seen.add(purl)
purls.push(purl)
}
}

if (purls.length === 0) {
return {
ok: false,
message: 'Provide at least one non-empty PURL with --purls.',
}
}

return { ok: true, purls }
}

export function matchesConfirmedPurls(
requested: readonly string[],
filters: unknown,
): boolean {
if (typeof filters !== 'object' || filters === null) {
return false
}
if (!('valid' in filters) || !('invalid' in filters)) {
return false
}
const { invalid, valid } = filters
if (
!Array.isArray(valid) ||
!valid.every(value => typeof value === 'string') ||
!Array.isArray(invalid) ||
!invalid.every(value => typeof value === 'string') ||
invalid.length > 0
) {
return false
}
const expected = new Set(requested)
const actual = new Set(valid)
return (
expected.size === requested.length &&
actual.size === valid.length &&
expected.size === actual.size &&
[...expected].every(value => actual.has(value))
)
}
Loading
Loading