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
77 changes: 61 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,20 @@ This guide walks you through both installation and usage.
2. [On Windows](#on-windows)
2. [Install Pre-Commit Hook](#install-pre-commit-hook)
3. [Cycode CLI Commands](#cycode-cli-commands)
4. [MCP Command](#mcp-command-experiment)
4. [Certificates and Proxies](#certificates-and-proxies)
5. [MCP Command](#mcp-command-experiment)
1. [Starting the MCP Server](#starting-the-mcp-server)
2. [Available Options](#available-options)
3. [MCP Tools](#mcp-tools)
4. [Usage Examples](#usage-examples)
5. [Advanced Configuration](#advanced-configuration)
5. [Platform Command](#platform-command-beta)
6. [Platform Command](#platform-command-beta)
1. [Discovering Commands](#discovering-commands)
2. [Examples](#platform-examples)
3. [Notes & Limitations](#platform-notes--limitations)
6. [AI Guardrails](#ai-guardrails-beta)
7. [AI Guardrails](#ai-guardrails-beta)
1. [Data Collected by AI Guardrails](#data-collected-by-ai-guardrails)
7. [Scan Command](#scan-command)
8. [Scan Command](#scan-command)
1. [Running a Scan](#running-a-scan)
1. [Options](#options)
1. [Severity Threshold](#severity-option)
Expand Down Expand Up @@ -64,11 +65,11 @@ This guide walks you through both installation and usage.
4. [Ignoring a Secret, IaC, or SCA Rule](#ignoring-a-secret-iac-sca-or-sast-rule)
5. [Ignoring a Package](#ignoring-a-package)
6. [Ignoring via a config file](#ignoring-via-a-config-file)
6. [Report command](#report-command)
9. [Report command](#report-command)
1. [Generating SBOM Report](#generating-sbom-report)
7. [Import command](#import-command)
8. [Scan logs](#scan-logs)
9. [Syntax Help](#syntax-help)
10. [Import command](#import-command)
11. [Scan logs](#scan-logs)
12. [Syntax Help](#syntax-help)

# Prerequisites

Expand Down Expand Up @@ -356,6 +357,51 @@ The following are the options and commands available with the Cycode CLI applica
| [report](#report-command) | Generate report. You will need to specify which report type to perform as SBOM. |
| status | Show the CLI status and exit. |

# Certificates and Proxies

By default, Cycode CLI verifies HTTPS connections against the CA bundle shipped with the CLI.

If your organization uses a proxy that inspects HTTPS traffic, or an on-premises installation with
its own CA, you have two options.

**Option 1 — use the certificates already installed on the machine.** If your CA is in the machine
certificate store (as is usually the case on a managed device), opt in:

```bash
export CYCODE_CLI_ENABLE_TRUSTSTORE=1
```

The CLI then verifies against the Windows certificate store, the macOS Keychain, or the system CA
directory on Linux, and no certificate paths need to be configured.

> [!IMPORTANT]
> This is opt-in on purpose. Trusting the machine store means trusting every root certificate
> present on that machine, including any an administrator or malicious software installed. Enable it
> when you know your machine's certificate store is one you trust.

**Option 2 — point the CLI at a CA bundle file.** Works without opting in:

| Environment Variable | Description |
|----------------------|-------------|
| `REQUESTS_CA_BUNDLE` | Path to a CA bundle file (`.pem` or `.crt`) to trust. |
| `CURL_CA_BUNDLE` | Alias for `REQUESTS_CA_BUNDLE`, honored when the latter is unset. |

The two options combine: with `CYCODE_CLI_ENABLE_TRUSTSTORE=1`, certificates from
`REQUESTS_CA_BUNDLE` are trusted *in addition to* the machine store, not instead of it.

> [!TIP]
> Run any command with `-v` to see which trust source is in use, for example `cycode -v status`.

Notes:

- `CYCODE_CLI_ENABLE_TRUSTSTORE` requires Python 3.10 or newer. On Python 3.9 the CLI logs a warning
and falls back to the bundled CA bundle; use `REQUESTS_CA_BUNDLE` instead, or upgrade Python. The
standalone executables and the Docker image already ship a supported Python.
- On Windows, the CLI has always fallen back to the system certificate store when neither
`REQUESTS_CA_BUNDLE` nor `CURL_CA_BUNDLE` is set. That behavior is unchanged.
- Proxies themselves are configured with the standard `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`
environment variables.

# MCP Command \[EXPERIMENT\]

> [!WARNING]
Expand Down Expand Up @@ -565,20 +611,20 @@ cycode mcp -t streamable-http -H 127.0.0.2 -p 9000 &
```

### Advanced Configuration
##### Custom Certificates and Timeouts (Proxy Environments)
##### Timeouts and Custom Certificates (Proxy Environments)

If your organization uses a corporate proxy or a custom CA bundle for HTTPS inspection, you need to tell Cycode CLI (and the underlying Python TLS stack) where to find the trusted certificate bundle. You can also increase the MCP tool call timeout if scans are being cut short.
If long-running scans are being cut short by your MCP client, increase the tool call timeout.

| Environment Variable | Description |
|----------------------|-------------|
| `REQUESTS_CA_BUNDLE` | Path to a custom CA bundle file (`.pem` or `.crt`). Used by the `requests` library for all HTTPS calls made by Cycode CLI. |
| `SSL_CERT_FILE` | Path to a custom CA bundle file. Used by Python's low-level `ssl` module. Set this alongside `REQUESTS_CA_BUNDLE` for full coverage. |
| `MCP_TOOL_TIMEOUT` | Timeout (in seconds) that MCP clients such as Claude and GitHub Copilot wait for a tool call to complete. Increase this if long-running scans are being cut off before they finish. |

> [!TIP]
> Set both `REQUESTS_CA_BUNDLE` and `SSL_CERT_FILE` to the same CA bundle path. `REQUESTS_CA_BUNDLE` covers the HTTP layer; `SSL_CERT_FILE` covers the lower-level TLS layer. Using only one may still cause certificate errors in some environments.
Behind a corporate proxy, set the certificate variables in the MCP server's `env` block. See
[Certificates and Proxies](#certificates-and-proxies) for the options: either
`CYCODE_CLI_ENABLE_TRUSTSTORE=1` to use the certificates already on the machine, or
`REQUESTS_CA_BUNDLE` pointing at a CA bundle file.

Example `mcp.json` configuration with custom certificates and a longer timeout:
Example `mcp.json` configuration with a custom CA bundle and a longer timeout:

```json
{
Expand All @@ -588,7 +634,6 @@ Example `mcp.json` configuration with custom certificates and a longer timeout:
"args": ["mcp"],
"env": {
"REQUESTS_CA_BUNDLE": "/path/to/your/corporate-ca-bundle.pem",
"SSL_CERT_FILE": "/path/to/your/corporate-ca-bundle.pem",
"MCP_TOOL_TIMEOUT": "1800"
}
}
Expand Down
1 change: 1 addition & 0 deletions cycode/cli/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@
LOGGING_LEVEL_ENV_VAR_NAME = 'LOGGING_LEVEL'
VERBOSE_ENV_VAR_NAME = 'CYCODE_CLI_VERBOSE'
DEBUG_ENV_VAR_NAME = 'CYCODE_CLI_DEBUG'
ENABLE_TRUSTSTORE_ENV_VAR_NAME = 'CYCODE_CLI_ENABLE_TRUSTSTORE'

CYCODE_CONFIGURATION_DIRECTORY: str = '.cycode'

Expand Down
17 changes: 14 additions & 3 deletions cycode/cli/exceptions/custom_exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from requests import Response

from cycode.cli import consts
from cycode.cli.models import CliError, CliErrors
from cycode.cli.utils import trust_store


class CycodeError(Exception):
Expand Down Expand Up @@ -96,6 +98,17 @@ def __str__(self) -> str:
return f'Error occurred while parsing terraform plan file. Path: {self.file_path}'


_SSL_ERROR_CA_BUNDLE_HINT = (
'set the REQUESTS_CA_BUNDLE (or CURL_CA_BUNDLE) environment variable to the path of a valid .pem or similar'
)
_SSL_ERROR_TRUST_HINT = (
'If you use an on-premises installation or a proxy that intercepts SSL traffic, '
f'set {consts.ENABLE_TRUSTSTORE_ENV_VAR_NAME}=1 to trust the CA certificates installed in your '
f'machine certificate store, or {_SSL_ERROR_CA_BUNDLE_HINT}'
if trust_store.is_supported() and not trust_store.is_enabled()
else f'If you use an on-premises installation or a proxy that intercepts SSL traffic, {_SSL_ERROR_CA_BUNDLE_HINT}'
)

KNOWN_USER_FRIENDLY_REQUEST_ERRORS: CliErrors = {
RequestHttpError: CliError(
soft_fail=True,
Expand All @@ -122,8 +135,6 @@ def __str__(self) -> str:
RequestSslError: CliError(
soft_fail=True,
code='ssl_error',
message='An SSL error occurred when trying to connect to the Cycode API. '
'If you use an on-premises installation or a proxy that intercepts SSL traffic '
'you should use the CURL_CA_BUNDLE environment variable to specify path to a valid .pem or similar',
message=f'An SSL error occurred when trying to connect to the Cycode API. {_SSL_ERROR_TRUST_HINT}',
),
}
66 changes: 66 additions & 0 deletions cycode/cli/utils/trust_store.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import sys

from cycode import config
from cycode.cli import consts
from cycode.logger import get_logger

logger = get_logger('Trust Store')

# truststore requires Python 3.10+, so on 3.9 the OS trust store is unavailable
_MIN_PYTHON_VERSION = (3, 10)

_installed = False


def is_supported() -> bool:
return sys.version_info >= _MIN_PYTHON_VERSION


def is_enabled() -> bool:
return config.get_val_as_bool(consts.ENABLE_TRUSTSTORE_ENV_VAR_NAME)


def is_installed() -> bool:
"""Whether the OS trust store has been injected into the TLS stack."""
return _installed


def install() -> bool:
"""Verify TLS against the machine's trust store.

`truststore.inject_into_ssl()` patches `ssl.SSLContext` process-wide, so this covers all HTTPS
traffic (the shared session, the presigned S3 upload, the version check) without touching call
sites. Certificates from `REQUESTS_CA_BUNDLE`/`CURL_CA_BUNDLE` keep working: requests still loads
them, and truststore treats them as additional trust anchors on top of the OS store.
"""
global _installed

if _installed:
return True

if not is_enabled():
logger.debug(
'OS trust store not enabled, using the bundled CA store (certifi). Set %s=1 to enable it',
consts.ENABLE_TRUSTSTORE_ENV_VAR_NAME,
)
return False

if not is_supported():
logger.warning(
'OS trust store requires Python %s+, using the bundled CA store (certifi). Current version: %s',
'.'.join(map(str, _MIN_PYTHON_VERSION)),
'.'.join(map(str, sys.version_info[:3])),
)
return False

try:
import truststore

truststore.inject_into_ssl()
except Exception as e:
logger.warning('Failed to use the OS trust store, falling back to the bundled CA store (certifi). %s', e)
return False

logger.debug('Using the OS trust store for TLS verification')
_installed = True
return True
13 changes: 9 additions & 4 deletions cycode/cyclient/cycode_client_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
RequestTimeoutError,
SlowUploadConnectionError,
)
from cycode.cli.utils import trust_store
from cycode.cyclient import config
from cycode.cyclient.headers import get_cli_user_agent, get_correlation_id
from cycode.cyclient.logger import logger
Expand All @@ -28,6 +29,8 @@


class SystemStorageSslContext(HTTPAdapter):
"""Windows system trust store path, used when truststore is not active."""

def init_poolmanager(self, *args, **kwargs) -> None:
default_context = ssl.create_default_context()
default_context.load_default_certs()
Expand All @@ -43,11 +46,13 @@ def cert_verify(self, *args, **kwargs) -> None:
@functools.cache
def _get_session() -> requests.Session:
"""Process-wide Session so TCP+TLS connections are reused across all API calls."""
trust_store.install()

session = requests.Session()
# On Windows without an explicit CA bundle env var, fall back to the system
# trust store via a custom SSL context.
if platform.system() == 'Windows' and not (
os.environ.get('REQUESTS_CA_BUNDLE') or os.environ.get('CURL_CA_BUNDLE')
if (
not trust_store.is_installed()
and platform.system() == 'Windows'
and not (os.environ.get('REQUESTS_CA_BUNDLE') or os.environ.get('CURL_CA_BUNDLE'))
):
session.mount('https://', SystemStorageSslContext())
return session
Expand Down
5 changes: 4 additions & 1 deletion cycode/cyclient/scan_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
SlowUploadConnectionError,
)
from cycode.cli.files_collector.models.in_memory_zip import InMemoryZip
from cycode.cli.utils import trust_store
from cycode.cyclient import models
from cycode.cyclient.cycode_client_base import CycodeClientBase, UploadProgressTracker
from cycode.cyclient.logger import logger
Expand Down Expand Up @@ -153,7 +154,9 @@ def upload_to_presigned_post(
tracker = UploadProgressTracker(prepared.body, on_upload_progress)

try:
# We are not using Cycode client, as we are calling aws S3.
# We are not using Cycode client, as we are calling aws S3. That also means this call
# skips the shared session, so the OS trust store has to be installed explicitly here.
trust_store.install()
response = requests.post(
url,
data=tracker,
Expand Down
15 changes: 14 additions & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions pyinstaller.spec
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import os
import platform
import subprocess
import sys

_INIT_FILE_PATH = os.path.join('cycode', '__init__.py')
_CODESIGN_IDENTITY = os.environ.get('APPLE_CERT_NAME')
Expand Down Expand Up @@ -41,6 +42,11 @@ _hiddenimports = [
'cycode.cli.apps.mcp',
]

# truststore is imported lazily inside cycode/cli/utils/trust_store.py, and it picks its platform
# backend behind a sys.platform branch. Only the current platform's backend actually resolves.
if sys.version_info >= (3, 10):
_hiddenimports += ['truststore', 'truststore._windows', 'truststore._macos', 'truststore._openssl']

a = Analysis(
scripts=['cycode/cli/main.py'],
excludes=['tests', 'setuptools', 'pkg_resources'],
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ patch-ng = "1.19.1"
typer = "^0.15.3"
tenacity = ">=9.1.2,<9.2.0"
mcp = { version = ">=1.28.1,<2.0.0", markers = "python_version >= '3.10'" }
truststore = { version = ">=0.10.4,<0.11.0", markers = "python_version >= '3.10'" }
pydantic = ">=2.11.5,<3.0.0"
pathvalidate = ">=3.3.1,<4.0.0"
tomli-w = ">=1.0.0,<2.0.0"
Expand Down
16 changes: 16 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import responses

from cycode.cli.user_settings.credentials_manager import CredentialsManager
from cycode.cli.utils import trust_store
from cycode.cyclient.client_creator import create_scan_client
from cycode.cyclient.cycode_oidc_based_client import CycodeOidcBasedClient
from cycode.cyclient.cycode_token_based_client import CycodeTokenBasedClient
Expand Down Expand Up @@ -118,3 +119,18 @@ def oidc_api_token_response(oidc_api_token_url: str) -> responses.Response:
},
status=200,
)


@pytest.fixture(autouse=True)
def _no_real_trust_store_injection(request: pytest.FixtureRequest, monkeypatch: pytest.MonkeyPatch) -> None:
"""Never let the suite patch the global `ssl` module.

`trust_store.install()` runs lazily from `_get_session()`, so any test that builds a session
would otherwise really call `truststore.inject_into_ssl()` — process-wide, leaking across tests.
It also fails noisily under pyfakefs, where truststore's platform probe cannot read the files
it expects. tests/utils/test_trust_store.py opts out: it tests install() itself, with the
truststore module stubbed.
"""
if request.module.__name__.rsplit('.', 1)[-1] == 'test_trust_store':
return
monkeypatch.setattr(trust_store, 'install', lambda: False)
Loading
Loading