Skip to content
Merged
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
7 changes: 2 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,12 @@
name: 'ci: native package'
run-name: 'ci: native package'

# PUSH, never pull_request. The fleet takes no outside contributions and
# lands on main directly, so a pull_request trigger adds no coverage a push
# trigger does not already give — and it is the fragile half: GitHub has
# narrowed pull_request defaults for security, and a silently non-firing
# trigger reads as a green repo with no CI at all.
on:
push:
branches: [main]
tags: ['*']
pull_request:
branches: [main]
workflow_dispatch:

permissions:
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ protection is no longer npm-only.
Browse the [command index](docs/commands.md) for every command and alias, then
open a focused guide from its tables.

See the [organization dependency inventory guide](docs/organization-dependencies.md)
for listing dependencies already known to your organization.

### MCP connections

For a local stdio connection, run `socket login` once, then configure your MCP
Expand Down
35 changes: 31 additions & 4 deletions docs/organization-dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ 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,
Expand All @@ -49,8 +74,10 @@ 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`. The
`--json` and `--markdown` options cannot be used together.
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

Expand All @@ -62,5 +89,5 @@ 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 and offset. On 1.x, it exits before making a request and prints
a generic dry-run message.
selected limit, offset, and PURL filters. On 1.x, it exits before making a
request and prints a generic dry-run message.
18 changes: 16 additions & 2 deletions src/command/organization/cmd-organization-dependencies.mts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { getOutputKind } from '../../util/output/mode.mjs'
import { hasDefaultApiToken } from '../../util/socket/sdk.mjs'
import { checkCommandInput } from '../../util/validation/input-check.mts'
import { parsePurlsOption } from './dependencies-filters.mts'

import type { CliCommandContext } from '../../util/cli/with-subcommands.mjs'
import type { MeowFlags } from '../../flags.mts'
Expand Down Expand Up @@ -48,7 +49,12 @@ export 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 @@ -65,6 +71,7 @@ export async function run(
Examples
${command}
${command} --limit 20 --offset 10
${command} --purls 'pkg:npm/minimist@0.0.4'
`,
}

Expand All @@ -75,7 +82,12 @@ export async function run(
importMeta,
})

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

const dryRun = cli.flags['dryRun']

Expand Down Expand Up @@ -110,6 +122,7 @@ export async function run(
outputDryRunFetch('organization dependencies', {
limit: validatedLimit || 50,
offset: validatedOffset,
...(purls ? { purls: JSON.stringify(purls) } : {}),
})
return
}
Expand All @@ -128,6 +141,7 @@ export async function run(
await handleDependencies({
limit: validatedLimit,
offset: validatedOffset,
purls,
outputKind,
})
}
6 changes: 4 additions & 2 deletions src/command/organization/dependencies-command.mts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,19 @@ import type { OutputKind } from '../../types.mts'
export async function handleDependencies({
limit,
offset,
purls,
outputKind,
}: {
limit: number
offset: number
purls: string[] | undefined
outputKind: OutputKind
}): Promise<void> {
debug(`Fetching dependencies with limit=${limit}, offset=${offset}`)
debugDir({ limit, offset, outputKind })
debugDir({ limit, offset, purls, outputKind })

const result = await fetchDependencies(
{ limit, offset },
{ limit, offset, purls },
{
commandPath: 'socket organization dependencies',
},
Expand Down
30 changes: 24 additions & 6 deletions src/command/organization/dependencies-data.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { handleApiCall } from '../../util/socket/api.mjs'
import { setupSdk } from '../../util/socket/sdk.mjs'
import { matchesConfirmedPurls } from './dependencies-filters.mts'

import type { CResult } from '../../types.mts'
import type { SetupSdkOptions } from '../../util/socket/sdk.mjs'
Expand All @@ -8,6 +9,7 @@ import type { SocketSdkSuccessResult } from '@socketsecurity/sdk-stable'
export type FetchDependenciesConfig = {
limit: number
offset: number
purls?: string[] | undefined
}

export type FetchDependenciesOptions = {
Expand All @@ -30,16 +32,32 @@ export async function fetchDependencies(
}
const sockSdk = sockSdkCResult.data

const { limit, offset } = {
__proto__: null,
...config,
} as FetchDependenciesConfig
const { limit, offset, purls } = config

return await handleApiCall<'searchDependencies'>(
sockSdk.searchDependencies({ limit, offset }),
const result = await handleApiCall<'searchDependencies'>(
sockSdk.searchDependencies({
limit,
offset,
...(purls === undefined ? {} : { purls }),
}),
{
commandPath,
description: 'organization dependencies',
},
)

if (
result.ok &&
purls !== undefined &&
!matchesConfirmedPurls(purls, result.data.purlFilters)
) {
return {
ok: false,
message: 'The server did not confirm every requested PURL filter.',
cause:
'Review the PURLs and try again. No dependency results were displayed because the filters may not have been applied.',
}
}

return result
}
97 changes: 97 additions & 0 deletions src/command/organization/dependencies-filters.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { getPurlObject } from '../../util/purl/parse.mts'

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

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 { valid, invalid } = 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))
)
}

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 parsePurlsOption(
argv: readonly string[],
suppliedPurls: readonly string[] | undefined,
): DependenciesPurlsResult {
const hasPurlsOption = argv.some(
argument => argument === '--purls' || argument.startsWith('--purls='),
)
if (!hasPurlsOption && suppliedPurls?.length === 0) {
return { ok: true, purls: undefined }
}
return parseDependenciesPurls(
hasPurlsOption && suppliedPurls === undefined ? [] : suppliedPurls,
)
}
Loading
Loading