cycode 3.12.3.dev5__py3-none-any.whl → 3.13.1.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
cycode/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = '3.12.3.dev5' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
1
+ __version__ = '3.13.1.dev1' # DON'T TOUCH. Placeholder. Will be filled automatically on poetry build from Git Tag
cycode/cli/app.py CHANGED
@@ -2,6 +2,7 @@ import logging
2
2
  import sys
3
3
  from typing import Annotated, Optional
4
4
 
5
+ import click
5
6
  import typer
6
7
  from typer import rich_utils
7
8
  from typer._completion_classes import completion_init
@@ -10,6 +11,7 @@ from typer.completion import install_callback, show_callback
10
11
 
11
12
  from cycode import __version__
12
13
  from cycode.cli.apps import ai_guardrails, ai_remediation, auth, configure, ignore, report, report_import, scan, status
14
+ from cycode.cli.apps.api import get_platform_group
13
15
 
14
16
  if sys.version_info >= (3, 10):
15
17
  from cycode.cli.apps import mcp
@@ -56,6 +58,27 @@ app.add_typer(status.app)
56
58
  if sys.version_info >= (3, 10):
57
59
  app.add_typer(mcp.app)
58
60
 
61
+ # Register the `platform` command group (dynamically built from the OpenAPI spec).
62
+ # The group itself is constructed cheaply at import time; the spec is only fetched
63
+ # when the user actually invokes `cycode platform ...`. Unrelated commands like
64
+ # `cycode scan` and `cycode status` never trigger a spec fetch.
65
+ #
66
+ # Typer doesn't support adding native Click groups directly, so we monkey-patch
67
+ # typer.main.get_group to inject our `platform` group into the resolved Click group.
68
+ # The `app_typer is app` guard ensures we only modify our own app.
69
+ _platform_group = get_platform_group()
70
+ _original_get_group = typer.main.get_group
71
+
72
+
73
+ def _get_group_with_platform(app_typer: typer.Typer) -> click.Group:
74
+ group = _original_get_group(app_typer)
75
+ if app_typer is app and _platform_group.name not in group.commands:
76
+ group.add_command(_platform_group, _platform_group.name)
77
+ return group
78
+
79
+
80
+ typer.main.get_group = _get_group_with_platform
81
+
59
82
 
60
83
  def check_latest_version_on_close(ctx: typer.Context) -> None:
61
84
  output = ctx.obj.get('output')
@@ -0,0 +1,69 @@
1
+ """Cycode platform API CLI commands.
2
+
3
+ Dynamically builds CLI command groups from the Cycode API v4 OpenAPI spec.
4
+ The spec is fetched lazily — only when the user invokes `cycode platform ...` —
5
+ and cached locally for 24 hours.
6
+ """
7
+
8
+ from typing import Any, Optional
9
+
10
+ import click
11
+
12
+ from cycode.logger import get_logger
13
+
14
+ logger = get_logger('Platform')
15
+
16
+ _PLATFORM_HELP = (
17
+ '[BETA] Access the Cycode platform.\n\n'
18
+ 'Commands are generated dynamically from the Cycode API spec and may change '
19
+ 'between releases. The spec is fetched on first use and cached for 24 hours.'
20
+ )
21
+
22
+
23
+ class PlatformGroup(click.Group):
24
+ """Lazy-loading Click group for `cycode platform` subcommands.
25
+
26
+ The OpenAPI spec is only fetched when the user actually invokes
27
+ `cycode platform ...` (or asks for its help). Unrelated commands like
28
+ `cycode scan` or `cycode status` never trigger a spec fetch.
29
+ """
30
+
31
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
32
+ super().__init__(*args, **kwargs)
33
+ self._loaded: bool = False
34
+
35
+ def _ensure_loaded(self, ctx: Optional[click.Context]) -> None:
36
+ if self._loaded:
37
+ return
38
+ self._loaded = True # set first to avoid re-entrancy on errors
39
+
40
+ client_id = client_secret = None
41
+ if ctx is not None:
42
+ root = ctx.find_root()
43
+ if root.obj:
44
+ client_id = root.obj.get('client_id')
45
+ client_secret = root.obj.get('client_secret')
46
+
47
+ try:
48
+ from cycode.cli.apps.api.api_command import build_api_command_groups
49
+
50
+ for sub_group, name in build_api_command_groups(client_id, client_secret):
51
+ if name not in self.commands:
52
+ self.add_command(sub_group, name)
53
+ except Exception as e:
54
+ logger.debug('Could not load platform commands: %s', e)
55
+ # Surface the error to the user only when they're inside `platform`
56
+ click.echo(f'Error loading Cycode platform commands: {e}', err=True)
57
+
58
+ def list_commands(self, ctx: click.Context) -> list[str]:
59
+ self._ensure_loaded(ctx)
60
+ return super().list_commands(ctx)
61
+
62
+ def get_command(self, ctx: click.Context, cmd_name: str) -> Optional[click.Command]:
63
+ self._ensure_loaded(ctx)
64
+ return super().get_command(ctx, cmd_name)
65
+
66
+
67
+ def get_platform_group() -> click.Group:
68
+ """Return the top-level `platform` Click group (lazy-loading)."""
69
+ return PlatformGroup(name='platform', help=_PLATFORM_HELP, no_args_is_help=True)
@@ -0,0 +1,271 @@
1
+ """OpenAPI-to-Typer translator: dynamically builds CLI commands from the Cycode API v4 spec."""
2
+
3
+ import json
4
+ import re
5
+ from typing import Any, Optional
6
+
7
+ import click
8
+
9
+ from cycode.cli.apps.api.openapi_spec import OpenAPISpecError, get_openapi_spec, parse_spec_commands
10
+ from cycode.logger import get_logger
11
+
12
+ logger = get_logger('API Command')
13
+
14
+ # Map OpenAPI parameter types to Click types
15
+ _CLICK_TYPE_MAP: dict[str, click.ParamType] = {
16
+ 'string': click.STRING,
17
+ 'integer': click.INT,
18
+ 'number': click.FLOAT,
19
+ 'boolean': click.BOOL,
20
+ }
21
+
22
+
23
+ def _normalize_tag(tag: str) -> str:
24
+ """Normalize an OpenAPI tag to a CLI-friendly command name.
25
+
26
+ 'Scan Statistics' -> 'scan-statistics'
27
+ 'CLI scan statistics' -> 'cli-scan-statistics'
28
+ """
29
+ return re.sub(r'[^a-z0-9]+', '-', tag.lower()).strip('-')
30
+
31
+
32
+ def _find_common_prefix(paths: list[str]) -> str:
33
+ """Find the longest common path prefix shared by all paths."""
34
+ if not paths:
35
+ return ''
36
+ if len(paths) == 1:
37
+ # For single-path tags, use the parent directory as prefix
38
+ return '/'.join(paths[0].split('/')[:-1])
39
+
40
+ common = paths[0]
41
+ for p in paths[1:]:
42
+ while not p.startswith(common + '/') and common != p:
43
+ common = '/'.join(common.split('/')[:-1])
44
+ return common
45
+
46
+
47
+ def _path_to_command_name(path: str, common_prefix: str, has_path_params: bool) -> str:
48
+ """Derive a CLI command name from an API path relative to the tag's common prefix.
49
+
50
+ Rules:
51
+ 1. Strip the common prefix shared by all endpoints in the tag
52
+ 2. Remove path parameter segments ({id})
53
+ 3. If nothing remains: 'list' (no path params) or 'view' (has path params)
54
+ 4. Otherwise: use remaining segments joined with hyphens
55
+
56
+ Examples:
57
+ /v4/projects (prefix=/v4/projects) -> list
58
+ /v4/projects/{id} (prefix=/v4/projects) -> view
59
+ /v4/projects/assets (prefix=/v4/projects) -> assets
60
+ /v4/violations/count (prefix=/v4/violations) -> count
61
+ """
62
+ # Strip common prefix
63
+ relative = path[len(common_prefix) :] if path.startswith(common_prefix) else path
64
+ relative = relative.strip('/')
65
+
66
+ # Remove path parameter segments and empty parts
67
+ parts = [p for p in relative.split('/') if p and not p.startswith('{')]
68
+
69
+ if not parts:
70
+ return 'view' if has_path_params else 'list'
71
+
72
+ # Join remaining segments with hyphens, normalize to kebab-case
73
+ return re.sub(r'[^a-z0-9]+', '-', '-'.join(parts).lower()).strip('-')
74
+
75
+
76
+ def _param_to_option_name(name: str) -> str:
77
+ """Convert an OpenAPI parameter name to a CLI option name.
78
+
79
+ 'page_size' -> '--page-size'
80
+ 'pageSize' -> '--page-size'
81
+ 'filter.status' -> '--filter-status'
82
+ """
83
+ s = re.sub(r'([a-z])([A-Z])', r'\1-\2', name)
84
+ # Replace any non-alphanumeric characters with hyphens
85
+ s = re.sub(r'[^a-z0-9]+', '-', s.lower()).strip('-')
86
+ return f'--{s}'
87
+
88
+
89
+ def _make_api_request(
90
+ endpoint_path: str,
91
+ method: str,
92
+ path_params: dict[str, str],
93
+ query_params: dict[str, Any],
94
+ client_id: Optional[str] = None,
95
+ client_secret: Optional[str] = None,
96
+ ) -> dict:
97
+ """Execute an API request using the CLI's standard auth client."""
98
+ from urllib.parse import quote
99
+
100
+ from cycode.cli.apps.api.openapi_spec import resolve_credentials
101
+ from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
102
+
103
+ cid, csecret = resolve_credentials(client_id, client_secret)
104
+ client = CycodeTokenBasedClient(cid, csecret)
105
+
106
+ # Substitute path parameters (URL-encoded to prevent path traversal)
107
+ url_path = endpoint_path
108
+ for param_name, param_value in path_params.items():
109
+ url_path = url_path.replace(f'{{{param_name}}}', quote(str(param_value), safe=''))
110
+
111
+ filtered_query = {k: v for k, v in query_params.items() if v is not None}
112
+
113
+ response = client.get(url_path.lstrip('/'), params=filtered_query)
114
+ return response.json()
115
+
116
+
117
+ def build_api_command_groups(
118
+ client_id: Optional[str] = None,
119
+ client_secret: Optional[str] = None,
120
+ ) -> list[tuple[click.Group, str]]:
121
+ """Build Click command groups from the OpenAPI spec.
122
+
123
+ Returns a list of (click_group, command_name) tuples.
124
+ """
125
+ try:
126
+ spec = get_openapi_spec(client_id, client_secret)
127
+ except OpenAPISpecError as e:
128
+ logger.warning('Could not load OpenAPI spec: %s', e)
129
+ return []
130
+
131
+ groups = parse_spec_commands(spec)
132
+ result = []
133
+
134
+ for tag, endpoints in groups.items():
135
+ tag_name = _normalize_tag(tag)
136
+
137
+ group = click.Group(name=tag_name, help=f'[BETA] {tag}')
138
+
139
+ # Compute common prefix from all GET (non-deprecated) endpoint paths in this tag
140
+ get_endpoints = [ep for ep in endpoints if ep['method'] == 'get' and not ep.get('deprecated')]
141
+ if not get_endpoints:
142
+ continue
143
+
144
+ clean_paths = [re.sub(r'/\{[^}]+\}', '', ep['path']) for ep in get_endpoints]
145
+ common_prefix = _find_common_prefix(clean_paths)
146
+
147
+ used_names: dict[str, int] = {}
148
+
149
+ for endpoint in get_endpoints:
150
+ has_path_params = bool(endpoint['path_params'])
151
+ cmd_name = _path_to_command_name(endpoint['path'], common_prefix, has_path_params)
152
+
153
+ # Fix redundancy: if command name matches the tag name, use list/view
154
+ # e.g. "cycode groups groups" -> "cycode groups list"
155
+ if cmd_name == tag_name:
156
+ cmd_name = 'view' if has_path_params else 'list'
157
+
158
+ # Handle duplicate names (e.g. deprecated + new endpoint for same resource)
159
+ if cmd_name in used_names:
160
+ used_names[cmd_name] += 1
161
+ cmd_name = f'{cmd_name}-v{used_names[cmd_name]}'
162
+ else:
163
+ used_names[cmd_name] = 1
164
+
165
+ cmd = _build_endpoint_command(cmd_name, endpoint)
166
+ group.add_command(cmd, cmd_name)
167
+
168
+ result.append((group, tag_name))
169
+
170
+ return result
171
+
172
+
173
+ def _build_click_params(endpoint: dict) -> list[click.Parameter]:
174
+ """Build Click parameters from OpenAPI endpoint definition."""
175
+ params: list[click.Parameter] = []
176
+
177
+ # Path parameters -> required arguments
178
+ for p in endpoint['path_params']:
179
+ param_type = _CLICK_TYPE_MAP.get(p.get('schema', {}).get('type', 'string'), click.STRING)
180
+ params.append(
181
+ click.Argument(
182
+ [p['name'].replace('-', '_')],
183
+ type=param_type,
184
+ required=True,
185
+ )
186
+ )
187
+
188
+ # Query parameters -> --option flags
189
+ for p in endpoint['query_params']:
190
+ param_type = _CLICK_TYPE_MAP.get(p.get('schema', {}).get('type', 'string'), click.STRING)
191
+ option_name = _param_to_option_name(p['name'])
192
+ required = p.get('required', False)
193
+ default = p.get('schema', {}).get('default')
194
+
195
+ schema = p.get('schema', {})
196
+ if 'enum' in schema:
197
+ param_type = click.Choice(schema['enum'])
198
+
199
+ params.append(
200
+ click.Option(
201
+ [option_name],
202
+ type=param_type,
203
+ required=required,
204
+ default=default,
205
+ help=p.get('description', ''),
206
+ show_default=default is not None,
207
+ )
208
+ )
209
+
210
+ return params
211
+
212
+
213
+ def _build_endpoint_command(cmd_name: str, endpoint: dict) -> click.Command:
214
+ """Build a Click command for an API endpoint.
215
+
216
+ Path parameters become required CLI arguments.
217
+ Query parameters become --option flags with proper types.
218
+ """
219
+ ep_path = endpoint['path']
220
+ ep_method = endpoint['method']
221
+ ep_path_params = list(endpoint['path_params'])
222
+ ep_query_params = list(endpoint['query_params'])
223
+ ep_description = endpoint['description'] or endpoint['summary']
224
+
225
+ # Build a mapping from Click's normalized kwarg name to original OpenAPI param name
226
+ _path_param_map = {p['name'].replace('-', '_').lower(): p['name'] for p in ep_path_params}
227
+ _query_param_map = {re.sub(r'[^a-z0-9]+', '_', p['name'].lower()).strip('_'): p['name'] for p in ep_query_params}
228
+
229
+ def _callback(**kwargs: Any) -> None:
230
+ ctx = click.get_current_context()
231
+
232
+ # Extract path param values using the mapping
233
+ path_values = {}
234
+ for kwarg_key, original_name in _path_param_map.items():
235
+ if kwarg_key in kwargs and kwargs[kwarg_key] is not None:
236
+ path_values[original_name] = kwargs[kwarg_key]
237
+
238
+ # Extract query param values (skip None)
239
+ query_values = {}
240
+ for kwarg_key, original_name in _query_param_map.items():
241
+ value = kwargs.get(kwarg_key)
242
+ if value is not None:
243
+ query_values[original_name] = value
244
+
245
+ # Get auth from root context (set by app_callback)
246
+ root_ctx = ctx.find_root()
247
+ client_id = root_ctx.obj.get('client_id') if root_ctx.obj else None
248
+ client_secret = root_ctx.obj.get('client_secret') if root_ctx.obj else None
249
+
250
+ try:
251
+ result = _make_api_request(
252
+ ep_path,
253
+ ep_method,
254
+ path_values,
255
+ query_values,
256
+ client_id=client_id,
257
+ client_secret=client_secret,
258
+ )
259
+ except Exception as e:
260
+ click.echo(f'Error: {e}', err=True)
261
+ raise click.Abort from e
262
+
263
+ click.echo(json.dumps(result, indent=2))
264
+
265
+ return click.Command(
266
+ name=cmd_name,
267
+ callback=_callback,
268
+ help=ep_description,
269
+ short_help=endpoint['summary'],
270
+ params=_build_click_params(endpoint),
271
+ )
@@ -0,0 +1,182 @@
1
+ """OpenAPI spec manager: fetch, cache, and parse the Cycode API v4 spec."""
2
+
3
+ import json
4
+ import os
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Optional
8
+
9
+ from cycode.cli.consts import CYCODE_CONFIGURATION_DIRECTORY
10
+ from cycode.cli.user_settings.credentials_manager import CredentialsManager
11
+ from cycode.cyclient import config as cyclient_config
12
+ from cycode.logger import get_logger
13
+
14
+ logger = get_logger('OpenAPI Spec')
15
+
16
+ _CACHE_DIR = Path.home() / CYCODE_CONFIGURATION_DIRECTORY
17
+ _CACHE_FILE = _CACHE_DIR / 'openapi-spec.json'
18
+ _CACHE_TTL_SECONDS = int(os.getenv('CYCODE_SPEC_CACHE_TTL', str(24 * 60 * 60))) # 24h default
19
+
20
+ _OPENAPI_SPEC_PATH = '/v4/api-docs/cycode-api-swagger.json'
21
+
22
+
23
+ def get_openapi_spec(client_id: Optional[str] = None, client_secret: Optional[str] = None) -> dict:
24
+ """Get the OpenAPI spec, using cache if fresh, otherwise fetching from API.
25
+
26
+ The spec is only fetched when the user actually invokes `cycode platform ...`.
27
+ Fetch uses the HTTP client's default timeout; on a slow connection the first
28
+ invocation will block accordingly. Once cached, subsequent invocations within
29
+ the TTL are near-instant.
30
+
31
+ Args:
32
+ client_id: Optional client ID override (from CLI flags).
33
+ client_secret: Optional client secret override (from CLI flags).
34
+
35
+ Returns:
36
+ Parsed OpenAPI specification dictionary.
37
+
38
+ Raises:
39
+ OpenAPISpecError: If spec cannot be loaded from cache or API.
40
+ """
41
+ cached = _load_cached_spec()
42
+ if cached is not None:
43
+ return cached
44
+
45
+ return _fetch_and_cache_spec(client_id, client_secret)
46
+
47
+
48
+ def _load_cached_spec() -> Optional[dict]:
49
+ """Load spec from local cache if it exists and is fresh."""
50
+ if not _CACHE_FILE.exists():
51
+ return None
52
+
53
+ try:
54
+ mtime = _CACHE_FILE.stat().st_mtime
55
+ if time.time() - mtime > _CACHE_TTL_SECONDS:
56
+ logger.debug('Cached OpenAPI spec is stale (age > %ds)', _CACHE_TTL_SECONDS)
57
+ return None
58
+
59
+ spec = json.loads(_CACHE_FILE.read_text(encoding='utf-8'))
60
+ logger.debug('Using cached OpenAPI spec from %s', _CACHE_FILE)
61
+ return spec
62
+ except Exception as e:
63
+ logger.warning('Failed to load cached OpenAPI spec: %s', e)
64
+ return None
65
+
66
+
67
+ def resolve_credentials(client_id: Optional[str] = None, client_secret: Optional[str] = None) -> tuple[str, str]:
68
+ """Resolve credentials from args or the CLI's standard credential chain."""
69
+ if not client_id or not client_secret:
70
+ credentials_manager = CredentialsManager()
71
+ cred_id, cred_secret = credentials_manager.get_credentials()
72
+ client_id = client_id or cred_id
73
+ client_secret = client_secret or cred_secret
74
+
75
+ if not client_id or not client_secret:
76
+ raise OpenAPISpecError(
77
+ 'Cycode credentials not found. Run `cycode auth` first, '
78
+ 'or set CYCODE_CLIENT_ID and CYCODE_CLIENT_SECRET environment variables.'
79
+ )
80
+
81
+ return client_id, client_secret
82
+
83
+
84
+ def _fetch_and_cache_spec(client_id: Optional[str] = None, client_secret: Optional[str] = None) -> dict:
85
+ """Fetch OpenAPI spec from API and cache to disk.
86
+
87
+ Uses CycodeTokenBasedClient for auth and retries. The spec is served from the app URL,
88
+ so we create a client with app_url as base instead of the default api_url.
89
+ """
90
+ from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
91
+
92
+ cid, csecret = resolve_credentials(client_id, client_secret)
93
+
94
+ # The spec is served from app.cycode.com, but token refresh POSTs to api.cycode.com.
95
+ # Ensure the token is fresh BEFORE overriding the base URL so that refresh
96
+ # targets the correct host.
97
+ client = CycodeTokenBasedClient(cid, csecret)
98
+ client.get_access_token()
99
+ client.api_url = cyclient_config.cycode_app_url
100
+
101
+ spec_path = _OPENAPI_SPEC_PATH.lstrip('/')
102
+ logger.info('Fetching OpenAPI spec from %s/%s', cyclient_config.cycode_app_url, spec_path)
103
+
104
+ try:
105
+ response = client.get(spec_path)
106
+ spec = response.json()
107
+ except Exception as e:
108
+ raise OpenAPISpecError(
109
+ f'Failed to fetch OpenAPI spec. Check your authentication and network connectivity. Error: {e}'
110
+ ) from e
111
+
112
+ if not isinstance(spec, dict) or 'paths' not in spec:
113
+ raise OpenAPISpecError('Response does not look like a valid OpenAPI spec (missing "paths" key).')
114
+
115
+ # Override server URL with API URL (supports on-premise installations)
116
+ spec['servers'] = [{'url': cyclient_config.cycode_api_url}]
117
+
118
+ # Cache to disk
119
+ _cache_spec(spec)
120
+
121
+ return spec
122
+
123
+
124
+ def _cache_spec(spec: dict) -> None:
125
+ """Write spec to local cache file atomically (write to temp file, then rename)."""
126
+ try:
127
+ _CACHE_DIR.mkdir(parents=True, exist_ok=True)
128
+ tmp_file = _CACHE_FILE.with_suffix('.json.tmp')
129
+ tmp_file.write_text(json.dumps(spec), encoding='utf-8')
130
+ tmp_file.replace(_CACHE_FILE) # atomic on POSIX and Windows
131
+ logger.debug('Cached OpenAPI spec to %s', _CACHE_FILE)
132
+ except Exception as e:
133
+ logger.warning('Failed to cache OpenAPI spec: %s', e)
134
+
135
+
136
+ def parse_spec_commands(spec: dict) -> dict[str, list[dict]]:
137
+ """Parse OpenAPI spec into resource groups with their endpoints.
138
+
139
+ Groups endpoints by their first tag, returning a dict of:
140
+ {tag_name: [endpoint_info, ...]}
141
+
142
+ Each endpoint_info contains:
143
+ - path: API path (e.g., '/v4/projects/{projectId}')
144
+ - method: HTTP method (e.g., 'get')
145
+ - summary: Human-readable summary
146
+ - description: Detailed description
147
+ - operation_id: Unique operation ID
148
+ - path_params: List of path parameter definitions
149
+ - query_params: List of query parameter definitions
150
+ """
151
+ groups: dict[str, list[dict]] = {}
152
+
153
+ for path, methods in spec.get('paths', {}).items():
154
+ for method, details in methods.items():
155
+ tags = details.get('tags', ['other'])
156
+ tag = tags[0] if tags else 'other'
157
+
158
+ # Separate path and query parameters
159
+ parameters = details.get('parameters', [])
160
+ path_params = [p for p in parameters if p.get('in') == 'path']
161
+ query_params = [p for p in parameters if p.get('in') == 'query']
162
+
163
+ endpoint_info = {
164
+ 'path': path,
165
+ 'method': method,
166
+ 'summary': details.get('summary', ''),
167
+ 'description': details.get('description', ''),
168
+ 'operation_id': details.get('operationId', ''),
169
+ 'path_params': path_params,
170
+ 'query_params': query_params,
171
+ 'deprecated': details.get('deprecated', False),
172
+ }
173
+
174
+ if tag not in groups:
175
+ groups[tag] = []
176
+ groups[tag].append(endpoint_info)
177
+
178
+ return groups
179
+
180
+
181
+ class OpenAPISpecError(Exception):
182
+ """Raised when the OpenAPI spec cannot be loaded."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cycode
3
- Version: 3.12.3.dev5
3
+ Version: 3.13.1.dev1
4
4
  Summary: Boost security in your dev lifecycle via SAST, SCA, Secrets & IaC scanning.
5
5
  License-Expression: MIT
6
6
  License-File: LICENCE
@@ -62,7 +62,11 @@ This guide walks you through both installation and usage.
62
62
  2. [Available Options](#available-options)
63
63
  3. [MCP Tools](#mcp-tools)
64
64
  4. [Usage Examples](#usage-examples)
65
- 5. [Scan Command](#scan-command)
65
+ 5. [Platform Command](#platform-command-beta)
66
+ 1. [Discovering Commands](#discovering-commands)
67
+ 2. [Examples](#platform-examples)
68
+ 3. [Notes & Limitations](#platform-notes--limitations)
69
+ 6. [Scan Command](#scan-command)
66
70
  1. [Running a Scan](#running-a-scan)
67
71
  1. [Options](#options)
68
72
  1. [Severity Threshold](#severity-option)
@@ -646,6 +650,64 @@ This information can be helpful when:
646
650
  - Debugging transport-specific issues
647
651
 
648
652
 
653
+ # Platform Command \[BETA\]
654
+
655
+ > [!WARNING]
656
+ > 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.
657
+
658
+ 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.
659
+
660
+ ```bash
661
+ cycode platform projects list --page-size 50
662
+ cycode platform violations count
663
+ cycode platform workflows view <workflow-id>
664
+ ```
665
+
666
+ 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.
667
+
668
+ > [!NOTE]
669
+ > 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.
670
+
671
+ ## Discovering Commands
672
+
673
+ Because commands are generated from the spec, the source of truth for what's available is `--help`:
674
+
675
+ ```bash
676
+ cycode platform --help # list all resource groups
677
+ cycode platform projects --help # list actions on a resource
678
+ cycode platform projects list --help # list options/arguments for an action
679
+ ```
680
+
681
+ ## Platform Examples
682
+
683
+ ```bash
684
+ # List projects with pagination
685
+ cycode platform projects list --page-size 25
686
+
687
+ # View a single project by ID
688
+ cycode platform projects view <project-id>
689
+
690
+ # Count violations across the tenant
691
+ cycode platform violations count
692
+
693
+ # Filter using query parameters (see `--help` for what each endpoint supports)
694
+ cycode platform violations list --severity CRITICAL
695
+ ```
696
+
697
+ All output is JSON by default — pipe it through `jq` for ad-hoc filtering:
698
+
699
+ ```bash
700
+ cycode platform projects list --page-size 100 | jq '.items[].name'
701
+ ```
702
+
703
+ ## Platform Notes & Limitations
704
+
705
+ - **Read-only today.** Only `GET` endpoints are exposed in this beta.
706
+ - **Spec-driven.** Adding a new endpoint to the API surfaces it automatically the next time the cache is refreshed.
707
+ - **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.
708
+ - **Override the cache TTL** with `CYCODE_SPEC_CACHE_TTL=<seconds>`.
709
+
710
+
649
711
  # Scan Command
650
712
 
651
713
  ## Running a Scan
@@ -1,7 +1,7 @@
1
- cycode/__init__.py,sha256=FYtGGWFeHBebJ4ZXbcB6omMDRUbH3lV8_EjHiWXUNbQ,115
1
+ cycode/__init__.py,sha256=XwAvpXUqHDlbSaLpeRzrQIJsv9a29URD8wVPjXnmVVo,115
2
2
  cycode/__main__.py,sha256=Z3bD5yrA7yPvAChcADQrqCaZd0ChGI1gdiwALwbWJ6U,104
3
3
  cycode/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- cycode/cli/app.py,sha256=bsfXV85RRb1iz19JRC9gkc5Iv30fnEE1cwA8dg552NQ,6482
4
+ cycode/cli/app.py,sha256=7ReEcVkRX9IaQ2I7jAj7Sl9smbtvxiuK8-9bitMEQik,7491
5
5
  cycode/cli/apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  cycode/cli/apps/activation_manager.py,sha256=Hz9PDJFB-ZmYi4HSG8iYC-fR8j5v25VuUU-l95Otsdk,1678
7
7
  cycode/cli/apps/ai_guardrails/__init__.py,sha256=R2l1CRRMOY4bAeJkndio81Sc4v0upURGo5s14Hejh-I,1120
@@ -26,6 +26,9 @@ cycode/cli/apps/ai_remediation/__init__.py,sha256=8vYthY9RQeJqEni3AIF5sryz8n-XJQ
26
26
  cycode/cli/apps/ai_remediation/ai_remediation_command.py,sha256=u1EdebaKCEmzv9fXmnIN0xDSLcCmGyjueYKvYfLOj_8,1549
27
27
  cycode/cli/apps/ai_remediation/apply_fix.py,sha256=9zgqiqF9HBQXi7Oz9ZIiANIAuKAMTji1PlNncCEOf5Q,817
28
28
  cycode/cli/apps/ai_remediation/print_remediation.py,sha256=nEVkR7gnGIryGEo0NOKzrmqsh4CjLr2QfVt9elsrzGY,590
29
+ cycode/cli/apps/api/__init__.py,sha256=1Re_qLdTwSt9H4OR-Cl5jxBvhNI4nEhPBzJ3-V1AMCw,2523
30
+ cycode/cli/apps/api/api_command.py,sha256=iZKOegd0-NQ8dOwxuQUFdem39iXd9nBGQr2FD-s4nyw,9539
31
+ cycode/cli/apps/api/openapi_spec.py,sha256=_Le9FN5JxL5XElEnBpKtdFLGZH2iSNVjXgQblxwFEXE,6877
29
32
  cycode/cli/apps/auth/__init__.py,sha256=rjf_rEBS1aS6rzY4Qh75BzOOX9SEHPdJMah-1FJM4DY,447
30
33
  cycode/cli/apps/auth/auth_command.py,sha256=lI5lXuyGD9_OOr-kdzVxu_3gDZasOHJ0mgMXSIt8-cs,1448
31
34
  cycode/cli/apps/auth/auth_common.py,sha256=bfQXqfv5bcYmc7njWOnG1VGzRU-C7spBv48gxHROCGU,2420
@@ -199,8 +202,8 @@ cycode/cyclient/report_client.py,sha256=Scq30NeJPzgXv0hPLO1U05AdE9i_2iu6cIrSKpEJ
199
202
  cycode/cyclient/scan_client.py,sha256=6TK5FQkfrvV7PHqRnUzEn1PBNd2oPYVamvIixcUfe3c,16755
200
203
  cycode/cyclient/scan_config_base.py,sha256=mXsPZGYCtp85rv5GIige40yQZXuRcEKUW-VQJ0vgFzk,1201
201
204
  cycode/logger.py,sha256=EfZGRK6VC5rE_LAjIcRrHFiQCueylCDXoG6bvGkrIME,2111
202
- cycode-3.12.3.dev5.dist-info/METADATA,sha256=LTrKb0Le7JLpLCLUjsO3MRgV3qhsA1w5nh33z1Grles,84350
203
- cycode-3.12.3.dev5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
204
- cycode-3.12.3.dev5.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
205
- cycode-3.12.3.dev5.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
206
- cycode-3.12.3.dev5.dist-info/RECORD,,
205
+ cycode-3.13.1.dev1.dist-info/METADATA,sha256=GxY_ST16ZL_6hONlPt3R_zxgdajGap5DW7jIbKfDUWg,87082
206
+ cycode-3.13.1.dev1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
207
+ cycode-3.13.1.dev1.dist-info/entry_points.txt,sha256=iDcVJM8ByLElVgvBgtYxDjw1kT7O8Mo0LcWZIT5L3Ig,45
208
+ cycode-3.13.1.dev1.dist-info/licenses/LICENCE,sha256=2Wx4N6mD_4xB7-E3hPkZ3MPhpJy__k_I8MaCSO-PDRo,1068
209
+ cycode-3.13.1.dev1.dist-info/RECORD,,