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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ 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
35 changes: 35 additions & 0 deletions docs/organization-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Organization dependency inventory

`socket organization dependencies` lists dependency records known to your
organization. It does not scan the current directory.

Each request returns one page. `--limit` defaults to 50 and `--offset` to 0.
Increase the offset by the limit until `Is there more data after this?` says
`no`:

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

## Filter by package

Pass a full package URL (PURL) to `--purls`. Repeat the flag to filter multiple
packages:

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

A versioned PURL matches that version. Without a version, it matches all indexed
versions. Filters do not support substrings, wildcards, ranges, qualifiers, or
subpaths.

Use `--json` for JSON output. Dependency records are in `data.rows`:

```sh
socket organization dependencies --json > dependencies.json
```
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