Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
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 Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ FROM base AS builder
ENV POETRY_VERSION=2.2.1

# deps are required to build cffi
RUN apk add --no-cache --virtual .build-deps gcc=14.2.0-r4 libffi-dev=3.4.7-r0 musl-dev=1.2.5-r9 && \
RUN apk add --no-cache --virtual .build-deps gcc=14.2.0-r4 libffi-dev=3.4.7-r0 musl-dev=1.2.5-r11 && \
pip install --no-cache-dir "poetry==$POETRY_VERSION" "poetry-dynamic-versioning[plugin]" && \
apk del .build-deps gcc libffi-dev musl-dev

Expand Down
64 changes: 63 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@ This guide walks you through both installation and usage.
2. [Available Options](#available-options)
3. [MCP Tools](#mcp-tools)
4. [Usage Examples](#usage-examples)
5. [Scan Command](#scan-command)
5. [Platform Command](#platform-command-beta)
1. [Discovering Commands](#discovering-commands)
2. [Examples](#platform-examples)
3. [Notes & Limitations](#platform-notes--limitations)
6. [Scan Command](#scan-command)
1. [Running a Scan](#running-a-scan)
1. [Options](#options)
1. [Severity Threshold](#severity-option)
Expand Down Expand Up @@ -605,6 +609,64 @@ This information can be helpful when:
- Debugging transport-specific issues


# Platform Command \[BETA\]

> [!WARNING]
> The `platform` command is in **beta**. Commands, arguments, and output formats are generated dynamically from the Cycode API spec and may change between releases without notice. Do not rely on them in production automation yet.

The `cycode platform` command exposes the Cycode platform's read APIs as CLI commands. It groups endpoints by resource (e.g. `projects`, `violations`, `workflows`) and turns each endpoint's parameters into typed CLI arguments and `--option` flags.

```bash
cycode platform projects list --page-size 50
cycode platform violations count
cycode platform workflows view <workflow-id>
```

The OpenAPI spec is fetched from the Cycode API on first use and cached at `~/.cycode/openapi-spec.json` for 24 hours. Unrelated commands (`cycode scan`, `cycode status`, etc.) do not trigger a fetch.

> [!NOTE]
> You must be authenticated (`cycode auth` or `CYCODE_CLIENT_ID` / `CYCODE_CLIENT_SECRET` environment variables) for `cycode platform` to discover and run commands. Other Cycode CLI commands work without authentication.

## Discovering Commands

Because commands are generated from the spec, the source of truth for what's available is `--help`:

```bash
cycode platform --help # list all resource groups
cycode platform projects --help # list actions on a resource
cycode platform projects list --help # list options/arguments for an action
```

## Platform Examples

```bash
# List projects with pagination
cycode platform projects list --page-size 25

# View a single project by ID
cycode platform projects view <project-id>

# Count violations across the tenant
cycode platform violations count

# Filter using query parameters (see `--help` for what each endpoint supports)
cycode platform violations list --severity CRITICAL
```

All output is JSON by default — pipe it through `jq` for ad-hoc filtering:

```bash
cycode platform projects list --page-size 100 | jq '.items[].name'
```

## Platform Notes & Limitations

- **Read-only today.** Only `GET` endpoints are exposed in this beta.
- **Spec-driven.** Adding a new endpoint to the API surfaces it automatically the next time the cache is refreshed.
- **No bundled spec.** The first `cycode platform` invocation after install (or after the 24h cache expires) performs a network fetch. On slow connections this first call may take a few seconds; subsequent calls are near-instant until the cache expires.
- **Override the cache TTL** with `CYCODE_SPEC_CACHE_TTL=<seconds>`.


# Scan Command

## Running a Scan
Expand Down
23 changes: 23 additions & 0 deletions cycode/cli/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import sys
from typing import Annotated, Optional

import click
import typer
from typer import rich_utils
from typer._completion_classes import completion_init
Expand All @@ -10,6 +11,7 @@

from cycode import __version__
from cycode.cli.apps import ai_guardrails, ai_remediation, auth, configure, ignore, report, report_import, scan, status
from cycode.cli.apps.api import get_platform_group

if sys.version_info >= (3, 10):
from cycode.cli.apps import mcp
Expand Down Expand Up @@ -56,6 +58,27 @@
if sys.version_info >= (3, 10):
app.add_typer(mcp.app)

# Register the `platform` command group (dynamically built from the OpenAPI spec).
# The group itself is constructed cheaply at import time; the spec is only fetched
# when the user actually invokes `cycode platform ...`. Unrelated commands like
# `cycode scan` and `cycode status` never trigger a spec fetch.
#
# Typer doesn't support adding native Click groups directly, so we monkey-patch
# typer.main.get_group to inject our `platform` group into the resolved Click group.
# The `app_typer is app` guard ensures we only modify our own app.
_platform_group = get_platform_group()
_original_get_group = typer.main.get_group


def _get_group_with_platform(app_typer: typer.Typer) -> click.Group:
group = _original_get_group(app_typer)
if app_typer is app and _platform_group.name not in group.commands:
group.add_command(_platform_group, _platform_group.name)
return group


typer.main.get_group = _get_group_with_platform


def check_latest_version_on_close(ctx: typer.Context) -> None:
output = ctx.obj.get('output')
Expand Down
69 changes: 69 additions & 0 deletions cycode/cli/apps/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Cycode platform API CLI commands.

Dynamically builds CLI command groups from the Cycode API v4 OpenAPI spec.
The spec is fetched lazily — only when the user invokes `cycode platform ...` —
and cached locally for 24 hours.
"""

from typing import Any, Optional

import click

from cycode.logger import get_logger

logger = get_logger('Platform')

_PLATFORM_HELP = (
'[BETA] Access the Cycode platform.\n\n'
'Commands are generated dynamically from the Cycode API spec and may change '
'between releases. The spec is fetched on first use and cached for 24 hours.'
)


class PlatformGroup(click.Group):
"""Lazy-loading Click group for `cycode platform` subcommands.

The OpenAPI spec is only fetched when the user actually invokes
`cycode platform ...` (or asks for its help). Unrelated commands like
`cycode scan` or `cycode status` never trigger a spec fetch.
"""

def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__(*args, **kwargs)
self._loaded: bool = False

def _ensure_loaded(self, ctx: Optional[click.Context]) -> None:
if self._loaded:
return
self._loaded = True # set first to avoid re-entrancy on errors

client_id = client_secret = None
if ctx is not None:
root = ctx.find_root()
if root.obj:
client_id = root.obj.get('client_id')
client_secret = root.obj.get('client_secret')

try:
from cycode.cli.apps.api.api_command import build_api_command_groups

for sub_group, name in build_api_command_groups(client_id, client_secret):
if name not in self.commands:
self.add_command(sub_group, name)
except Exception as e:
logger.debug('Could not load platform commands: %s', e)
# Surface the error to the user only when they're inside `platform`
click.echo(f'Error loading Cycode platform commands: {e}', err=True)

def list_commands(self, ctx: click.Context) -> list[str]:
self._ensure_loaded(ctx)
return super().list_commands(ctx)

def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
self._ensure_loaded(ctx)
return super().get_command(ctx, cmd_name)


def get_platform_group() -> click.Group:
"""Return the top-level `platform` Click group (lazy-loading)."""
return PlatformGroup(name='platform', help=_PLATFORM_HELP, no_args_is_help=True)
Loading
Loading