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
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